scorm-again 3.1.4 → 3.2.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.
Files changed (43) hide show
  1. package/README.md +3 -1
  2. package/dist/cross-frame-api.js +6 -13
  3. package/dist/cross-frame-api.js.map +1 -1
  4. package/dist/cross-frame-api.min.js +1 -1
  5. package/dist/cross-frame-lms.js +2 -6
  6. package/dist/cross-frame-lms.js.map +1 -1
  7. package/dist/esm/scorm-again.js +1128 -198
  8. package/dist/esm/scorm-again.js.map +1 -1
  9. package/dist/esm/scorm-again.min.js +1 -1
  10. package/dist/esm/scorm-again.min.js.map +1 -1
  11. package/dist/esm/scorm12.js +60 -13
  12. package/dist/esm/scorm12.js.map +1 -1
  13. package/dist/esm/scorm12.min.js +1 -1
  14. package/dist/esm/scorm12.min.js.map +1 -1
  15. package/dist/esm/scorm2004.js +1128 -198
  16. package/dist/esm/scorm2004.js.map +1 -1
  17. package/dist/esm/scorm2004.min.js +1 -1
  18. package/dist/esm/scorm2004.min.js.map +1 -1
  19. package/dist/scorm-again.js +2088 -1465
  20. package/dist/scorm-again.js.map +1 -1
  21. package/dist/scorm-again.min.js +1 -1
  22. package/dist/scorm12.js +347 -420
  23. package/dist/scorm12.js.map +1 -1
  24. package/dist/scorm12.min.js +1 -1
  25. package/dist/scorm2004.js +1983 -1301
  26. package/dist/scorm2004.js.map +1 -1
  27. package/dist/scorm2004.min.js +1 -1
  28. package/dist/types/BaseAPI.d.ts +1 -1
  29. package/dist/types/Scorm2004API.d.ts +11 -0
  30. package/dist/types/cmi/scorm2004/sequencing/activity.d.ts +47 -5
  31. package/dist/types/cmi/scorm2004/sequencing/handlers/rte_data_transfer.d.ts +1 -0
  32. package/dist/types/cmi/scorm2004/sequencing/objectives/global_objective_synchronizer.d.ts +17 -1
  33. package/dist/types/cmi/scorm2004/sequencing/sequencing_rules.d.ts +6 -1
  34. package/dist/types/cmi/scorm2004/sequencing/traversal/flow_traversal_service.d.ts +5 -2
  35. package/dist/types/constants/regex.d.ts +1 -0
  36. package/dist/types/interfaces/services.d.ts +1 -1
  37. package/dist/types/services/AsynchronousHttpService.d.ts +1 -0
  38. package/dist/types/services/ErrorHandlingService.d.ts +4 -4
  39. package/dist/types/services/SynchronousHttpService.d.ts +1 -0
  40. package/dist/types/types/api_types.d.ts +16 -0
  41. package/dist/types/types/sequencing_types.d.ts +7 -0
  42. package/dist/types/utilities/core.d.ts +6 -1
  43. package/package.json +4 -4
@@ -2,6 +2,23 @@ const SECONDS_PER_SECOND = 1;
2
2
  const SECONDS_PER_MINUTE = 60;
3
3
  const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
4
4
  const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
5
+ const CORS_SAFELISTED_CONTENT_TYPES = [
6
+ "text/plain",
7
+ "application/x-www-form-urlencoded",
8
+ "multipart/form-data"
9
+ ];
10
+ function isCorsSafelistedContentType(contentType) {
11
+ const essence = ((contentType || "").split(";")[0] ?? "").trim().toLowerCase();
12
+ return CORS_SAFELISTED_CONTENT_TYPES.includes(essence);
13
+ }
14
+ function isCrossOriginUrl(url) {
15
+ if (typeof location === "undefined" || !location || !location.origin) return false;
16
+ try {
17
+ return new URL(url, location.href).origin !== location.origin;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
5
22
  const designations = {
6
23
  D: SECONDS_PER_DAY,
7
24
  H: SECONDS_PER_HOUR,
@@ -139,12 +156,18 @@ function stringMatches(str, tester) {
139
156
  }
140
157
  return new RegExp(tester).test(str);
141
158
  }
142
- function memoize(fn, keyFn) {
159
+ function memoize(fn, keyFn, options) {
143
160
  const cache = /* @__PURE__ */ new Map();
144
161
  return ((...args) => {
145
162
  const key = keyFn ? keyFn(...args) : JSON.stringify(args);
163
+ if (options?.maxKeyLength !== void 0 && key.length > options.maxKeyLength) {
164
+ return fn(...args);
165
+ }
146
166
  return cache.has(key) ? cache.get(key) : (() => {
147
167
  const result = fn(...args);
168
+ if (options?.maxEntries !== void 0 && cache.size >= options.maxEntries) {
169
+ cache.delete(cache.keys().next().value);
170
+ }
148
171
  cache.set(key, result);
149
172
  return result;
150
173
  })();
@@ -730,8 +753,10 @@ const DefaultSettings = {
730
753
  lmsCommitUrl: false,
731
754
  dataCommitFormat: "json",
732
755
  commitRequestDataType: "application/json;charset=UTF-8",
756
+ terminationCommitContentType: "text/plain;charset=UTF-8",
733
757
  autoProgress: false,
734
758
  logLevel: LogLevelEnum.ERROR,
759
+ uninitializedGetLogLevel: LogLevelEnum.WARN,
735
760
  selfReportSessionTime: false,
736
761
  alwaysSendTotalTime: false,
737
762
  renderCommonCommitFields: false,
@@ -896,6 +921,8 @@ const scorm2004_regex = {
896
921
  CMILang: "^([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?$|^$",
897
922
  /** CMILangString250 - String with optional language tag, max 250 chars (RTE C.1.3) */
898
923
  CMILangString250: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).{0,250}$)?$",
924
+ /** CMILangString - Optional language tag, no length cap; RTE C.1.3 SPM is a floor and is deliberately not enforced */
925
+ CMILangString: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).*$)?$",
899
926
  /** CMILangcr - Language tag pattern with content */
900
927
  CMILangcr: "^(({lang=([a-zA-Z]{1,8}|i|x)?(-[a-zA-Z0-9-]{2,8})?}))(.*?)$",
901
928
  /** CMILangString250cr - String with optional language tag (carriage return variant) */
@@ -1548,12 +1575,6 @@ const HIDE_LMS_UI_TOKENS = [
1548
1575
  "suspendAll"
1549
1576
  ];
1550
1577
 
1551
- var RuleConditionOperator = /* @__PURE__ */ ((RuleConditionOperator2) => {
1552
- RuleConditionOperator2["NOT"] = "not";
1553
- RuleConditionOperator2["AND"] = "and";
1554
- RuleConditionOperator2["OR"] = "or";
1555
- return RuleConditionOperator2;
1556
- })(RuleConditionOperator || {});
1557
1578
  var RuleActionType = /* @__PURE__ */ ((RuleActionType2) => {
1558
1579
  RuleActionType2["SKIP"] = "skip";
1559
1580
  RuleActionType2["DISABLED"] = "disabled";
@@ -1568,6 +1589,48 @@ var RuleActionType = /* @__PURE__ */ ((RuleActionType2) => {
1568
1589
  RuleActionType2["EXIT"] = "exit";
1569
1590
  return RuleActionType2;
1570
1591
  })(RuleActionType || {});
1592
+ function kleeneNot(value) {
1593
+ if (value === "unknown") {
1594
+ return "unknown";
1595
+ }
1596
+ return !value;
1597
+ }
1598
+ function kleeneAnd(values) {
1599
+ let hasUnknown = false;
1600
+ for (const value of values) {
1601
+ if (value === false) {
1602
+ return false;
1603
+ }
1604
+ if (value === "unknown") {
1605
+ hasUnknown = true;
1606
+ }
1607
+ }
1608
+ return hasUnknown ? "unknown" : true;
1609
+ }
1610
+ function kleeneOr(values) {
1611
+ let hasUnknown = false;
1612
+ for (const value of values) {
1613
+ if (value === true) {
1614
+ return true;
1615
+ }
1616
+ if (value === "unknown") {
1617
+ hasUnknown = true;
1618
+ }
1619
+ }
1620
+ return hasUnknown ? "unknown" : false;
1621
+ }
1622
+ function combineRuleConditionResults(values, conditionCombination) {
1623
+ if (values.length === 0) {
1624
+ return false;
1625
+ }
1626
+ if (conditionCombination === "all" || conditionCombination === "and" /* AND */) {
1627
+ return kleeneAnd(values);
1628
+ }
1629
+ if (conditionCombination === "any" || conditionCombination === "or" /* OR */) {
1630
+ return kleeneOr(values);
1631
+ }
1632
+ return false;
1633
+ }
1571
1634
  class RuleCondition extends BaseCMI {
1572
1635
  _condition = "always" /* ALWAYS */;
1573
1636
  _operator = null;
@@ -1673,51 +1736,72 @@ class RuleCondition extends BaseCMI {
1673
1736
  /**
1674
1737
  * Evaluate the condition for an activity
1675
1738
  * @param {Activity} activity - The activity to evaluate the condition for
1676
- * @return {boolean} - True if the condition is met, false otherwise
1739
+ * @return {RuleConditionEvaluation} - True, false, or unknown per SCORM 2004 4th Ed.
1677
1740
  */
1678
1741
  evaluate(activity) {
1679
1742
  let result;
1743
+ const hasReferencedObjective = this._referencedObjective !== null;
1680
1744
  const referencedObjective = this.resolveReferencedObjective(activity);
1681
1745
  switch (this._condition) {
1682
1746
  case "satisfied" /* SATISFIED */:
1683
1747
  case "objectiveSatisfied" /* OBJECTIVE_SATISFIED */:
1684
- if (referencedObjective) {
1685
- result = referencedObjective.satisfiedStatus === true;
1748
+ if (hasReferencedObjective && !referencedObjective) {
1749
+ result = false;
1750
+ } else if (referencedObjective) {
1751
+ result = referencedObjective.satisfiedStatusKnown || referencedObjective.progressStatus ? referencedObjective.satisfiedStatus === true : "unknown";
1752
+ } else if (activity.objectiveSatisfiedStatusKnown) {
1753
+ result = activity.objectiveSatisfiedStatus === true;
1754
+ } else if (activity.successStatus !== SuccessStatus.UNKNOWN) {
1755
+ result = activity.successStatus === SuccessStatus.PASSED;
1686
1756
  } else {
1687
- result = activity.successStatus === SuccessStatus.PASSED || activity.objectiveSatisfiedStatus === true;
1757
+ result = "unknown";
1688
1758
  }
1689
1759
  break;
1690
1760
  case "objectiveStatusKnown" /* OBJECTIVE_STATUS_KNOWN */:
1691
- result = referencedObjective ? !!referencedObjective.measureStatus : !!activity.objectiveMeasureStatus;
1761
+ result = hasReferencedObjective && !referencedObjective ? false : referencedObjective ? !!referencedObjective.satisfiedStatusKnown : !!activity.objectiveSatisfiedStatusKnown;
1692
1762
  break;
1693
1763
  case "objectiveMeasureKnown" /* OBJECTIVE_MEASURE_KNOWN */:
1694
- result = referencedObjective ? !!referencedObjective.measureStatus : !!activity.objectiveMeasureStatus;
1764
+ result = hasReferencedObjective && !referencedObjective ? false : referencedObjective ? !!referencedObjective.measureStatus : !!activity.objectiveMeasureStatus;
1695
1765
  break;
1696
1766
  case "objectiveMeasureGreaterThan" /* OBJECTIVE_MEASURE_GREATER_THAN */: {
1767
+ if (hasReferencedObjective && !referencedObjective) {
1768
+ result = false;
1769
+ break;
1770
+ }
1697
1771
  const greaterThanValue = this._parameters.get("threshold") || 0;
1698
1772
  const measureStatus = referencedObjective ? referencedObjective.measureStatus : activity.objectiveMeasureStatus;
1699
1773
  const measureValue = referencedObjective ? referencedObjective.normalizedMeasure : activity.objectiveNormalizedMeasure;
1700
- result = !!measureStatus && measureValue > greaterThanValue;
1774
+ result = measureStatus ? measureValue > greaterThanValue : "unknown";
1701
1775
  break;
1702
1776
  }
1703
1777
  case "objectiveMeasureLessThan" /* OBJECTIVE_MEASURE_LESS_THAN */: {
1778
+ if (hasReferencedObjective && !referencedObjective) {
1779
+ result = false;
1780
+ break;
1781
+ }
1704
1782
  const lessThanValue = this._parameters.get("threshold") || 0;
1705
1783
  const measureStatus = referencedObjective ? referencedObjective.measureStatus : activity.objectiveMeasureStatus;
1706
1784
  const measureValue = referencedObjective ? referencedObjective.normalizedMeasure : activity.objectiveNormalizedMeasure;
1707
- result = !!measureStatus && measureValue < lessThanValue;
1785
+ result = measureStatus ? measureValue < lessThanValue : "unknown";
1708
1786
  break;
1709
1787
  }
1710
1788
  case "completed" /* COMPLETED */:
1711
1789
  case "activityCompleted" /* ACTIVITY_COMPLETED */:
1712
- if (referencedObjective) {
1713
- result = referencedObjective.completionStatus === CompletionStatus.COMPLETED;
1790
+ if (hasReferencedObjective && !referencedObjective) {
1791
+ result = false;
1792
+ } else if (referencedObjective) {
1793
+ result = referencedObjective.completionStatus === CompletionStatus.UNKNOWN ? "unknown" : referencedObjective.completionStatus === CompletionStatus.COMPLETED;
1794
+ } else if (activity.completionStatus === CompletionStatus.UNKNOWN) {
1795
+ result = "unknown";
1714
1796
  } else {
1715
- result = activity.isCompleted;
1797
+ result = activity.completionStatus === CompletionStatus.COMPLETED;
1716
1798
  }
1717
1799
  break;
1718
1800
  case "progressKnown" /* PROGRESS_KNOWN */:
1719
1801
  case "activityProgressKnown" /* ACTIVITY_PROGRESS_KNOWN */:
1720
- if (referencedObjective) {
1802
+ if (hasReferencedObjective && !referencedObjective) {
1803
+ result = false;
1804
+ } else if (referencedObjective) {
1721
1805
  result = referencedObjective.completionStatus !== CompletionStatus.UNKNOWN;
1722
1806
  } else {
1723
1807
  result = activity.completionStatus !== "unknown";
@@ -1746,7 +1830,7 @@ class RuleCondition extends BaseCMI {
1746
1830
  break;
1747
1831
  }
1748
1832
  if (this._operator === "not" /* NOT */) {
1749
- result = !result;
1833
+ result = kleeneNot(result);
1750
1834
  }
1751
1835
  return result;
1752
1836
  }
@@ -1953,15 +2037,10 @@ class SequencingRule extends BaseCMI {
1953
2037
  * @return {boolean} - True if the rule conditions are met, false otherwise
1954
2038
  */
1955
2039
  evaluate(activity) {
1956
- if (this._conditions.length === 0) {
1957
- return true;
1958
- }
1959
- if (this._conditionCombination === "all" || this._conditionCombination === "and" /* AND */) {
1960
- return this._conditions.every((condition) => condition.evaluate(activity));
1961
- } else if (this._conditionCombination === "any" || this._conditionCombination === "or" /* OR */) {
1962
- return this._conditions.some((condition) => condition.evaluate(activity));
1963
- }
1964
- return false;
2040
+ return combineRuleConditionResults(
2041
+ this._conditions.map((condition) => condition.evaluate(activity)),
2042
+ this._conditionCombination
2043
+ ) === true;
1965
2044
  }
1966
2045
  /**
1967
2046
  * toJSON for SequencingRule
@@ -2794,19 +2873,10 @@ class RuleEvaluationEngine {
2794
2873
  * Evaluates individual sequencing rule conditions
2795
2874
  * @param {Activity} activity - The activity to evaluate the rule for
2796
2875
  * @param {SequencingRule} rule - The rule to evaluate
2797
- * @return {boolean} - True if all rule conditions are met
2876
+ * @return {boolean} - True only when the rule evaluates to definite true
2798
2877
  */
2799
2878
  checkRuleSubprocess(activity, rule) {
2800
- if (rule.conditions.length === 0) {
2801
- return true;
2802
- }
2803
- const conditionCombination = rule.conditionCombination;
2804
- if (conditionCombination === "all" || conditionCombination === RuleConditionOperator.AND) {
2805
- return rule.conditions.every((condition) => condition.evaluate(activity));
2806
- } else if (conditionCombination === "any" || conditionCombination === RuleConditionOperator.OR) {
2807
- return rule.conditions.some((condition) => condition.evaluate(activity));
2808
- }
2809
- return false;
2879
+ return rule.evaluate(activity);
2810
2880
  }
2811
2881
  /**
2812
2882
  * Exit Action Rules Subprocess (TB.2.1)
@@ -3678,16 +3748,21 @@ class FlowTraversalService {
3678
3748
  * @param {Activity} fromActivity - The activity to flow from
3679
3749
  * @param {FlowSubprocessMode} direction - The flow direction
3680
3750
  * @return {FlowSubprocessResult} - Result containing the deliverable activity
3751
+ * @spec SN Book: SB.2.3 (Flow Subprocess) - preserves the SB.2.1 effective traversal direction for SB.2.2.
3752
+ * @spec SN Book: SB.2.2 (Flow Activity Traversal Subprocess) - evaluates candidates using the effective direction returned by SB.2.1.
3681
3753
  */
3682
3754
  flowSubprocess(fromActivity, direction) {
3683
3755
  let candidateActivity = fromActivity;
3684
3756
  let firstIteration = true;
3685
3757
  let lastCandidateHadNoChildren = false;
3758
+ let currentDirection = direction;
3759
+ let forwardOnlyCluster = null;
3686
3760
  while (candidateActivity) {
3687
3761
  const traversalResult = this.flowTreeTraversalSubprocess(
3688
3762
  candidateActivity,
3689
- direction,
3690
- firstIteration
3763
+ currentDirection,
3764
+ firstIteration,
3765
+ forwardOnlyCluster
3691
3766
  );
3692
3767
  if (!traversalResult.activity) {
3693
3768
  let exceptionCode = null;
@@ -3705,17 +3780,22 @@ class FlowTraversalService {
3705
3780
  traversalResult.endSequencingSession
3706
3781
  );
3707
3782
  }
3783
+ const effectiveDirection = traversalResult.direction || currentDirection;
3784
+ if (traversalResult.forwardOnlyCluster) {
3785
+ forwardOnlyCluster = traversalResult.forwardOnlyCluster;
3786
+ }
3708
3787
  lastCandidateHadNoChildren = traversalResult.activity.children.length > 0 && traversalResult.activity.getAvailableChildren().length === 0;
3709
3788
  const deliverable = this.flowActivityTraversalSubprocess(
3710
3789
  traversalResult.activity,
3711
- direction === FlowSubprocessMode.FORWARD,
3790
+ effectiveDirection === FlowSubprocessMode.FORWARD,
3712
3791
  true,
3713
- direction
3792
+ effectiveDirection
3714
3793
  );
3715
3794
  if (deliverable) {
3716
3795
  return new FlowSubprocessResult(deliverable, true, null, false);
3717
3796
  }
3718
3797
  candidateActivity = traversalResult.activity;
3798
+ currentDirection = effectiveDirection;
3719
3799
  firstIteration = false;
3720
3800
  }
3721
3801
  return new FlowSubprocessResult(null, false, null, false);
@@ -3726,11 +3806,13 @@ class FlowTraversalService {
3726
3806
  * @param {Activity} fromActivity - The activity to traverse from
3727
3807
  * @param {FlowSubprocessMode} direction - The traversal direction
3728
3808
  * @param {boolean} skipChildren - Whether to skip checking children
3809
+ * @param {Activity | null} forwardTraversalBoundary - Cluster boundary for an SB.2.1 forwardOnly direction reversal
3729
3810
  * @return {FlowTreeTraversalResult} - The next activity and flags
3811
+ * @spec SN Book: SB.2.1 (Flow Tree Traversal Subprocess) - backward traversal into a forwardOnly cluster selects the first available child and reverses traversal direction to Forward.
3730
3812
  */
3731
- flowTreeTraversalSubprocess(fromActivity, direction, skipChildren = false) {
3813
+ flowTreeTraversalSubprocess(fromActivity, direction, skipChildren = false, forwardTraversalBoundary = null) {
3732
3814
  if (direction === FlowSubprocessMode.FORWARD) {
3733
- return this.traverseForward(fromActivity, skipChildren);
3815
+ return this.traverseForward(fromActivity, skipChildren, forwardTraversalBoundary);
3734
3816
  } else {
3735
3817
  return this.traverseBackward(fromActivity);
3736
3818
  }
@@ -3739,10 +3821,15 @@ class FlowTraversalService {
3739
3821
  * Traverse forward in the activity tree
3740
3822
  * @param {Activity} fromActivity - Starting activity
3741
3823
  * @param {boolean} skipChildren - Whether to skip children
3824
+ * @param {Activity | null} forwardTraversalBoundary - Cluster boundary for an SB.2.1 forwardOnly direction reversal
3742
3825
  * @return {FlowTreeTraversalResult}
3826
+ * @spec SN Book: SB.2.1 (Flow Tree Traversal Subprocess) - a reversed Forward traversal from a forwardOnly cluster remains within that cluster.
3743
3827
  */
3744
- traverseForward(fromActivity, skipChildren) {
3745
- if (skipChildren && this.isActivityLastOverall(fromActivity)) {
3828
+ traverseForward(fromActivity, skipChildren, forwardTraversalBoundary = null) {
3829
+ if (forwardTraversalBoundary && !this.isDescendantOfOrSelf(fromActivity, forwardTraversalBoundary)) {
3830
+ return { activity: null, endSequencingSession: false };
3831
+ }
3832
+ if (!forwardTraversalBoundary && skipChildren && this.isActivityLastOverall(fromActivity)) {
3746
3833
  if (this.activityTree.root) {
3747
3834
  this.terminateDescendentAttempts(this.activityTree.root);
3748
3835
  }
@@ -3761,6 +3848,9 @@ class FlowTraversalService {
3761
3848
  if (nextSibling) {
3762
3849
  return { activity: nextSibling, endSequencingSession: false };
3763
3850
  }
3851
+ if (forwardTraversalBoundary && (current === forwardTraversalBoundary || current.parent === forwardTraversalBoundary)) {
3852
+ return { activity: null, endSequencingSession: false };
3853
+ }
3764
3854
  current = current.parent;
3765
3855
  }
3766
3856
  if (this.activityTree.root) {
@@ -3772,6 +3862,7 @@ class FlowTraversalService {
3772
3862
  * Traverse backward in the activity tree
3773
3863
  * @param {Activity} fromActivity - Starting activity
3774
3864
  * @return {FlowTreeTraversalResult}
3865
+ * @spec SN Book: SB.2.1 (Flow Tree Traversal Subprocess) - backward traversal into a forwardOnly cluster selects the first available child and reverses traversal direction to Forward.
3775
3866
  */
3776
3867
  traverseBackward(fromActivity) {
3777
3868
  if (fromActivity.parent && fromActivity.parent.sequencingControls.forwardOnly) {
@@ -3779,10 +3870,7 @@ class FlowTraversalService {
3779
3870
  }
3780
3871
  const previousSibling = this.activityTree.getPreviousSibling(fromActivity);
3781
3872
  if (previousSibling) {
3782
- return {
3783
- activity: this.getLastDescendant(previousSibling),
3784
- endSequencingSession: false
3785
- };
3873
+ return this.getBackwardTraversalEntry(previousSibling);
3786
3874
  }
3787
3875
  let current = fromActivity;
3788
3876
  let ancestorIterations = 0;
@@ -3793,38 +3881,64 @@ class FlowTraversalService {
3793
3881
  }
3794
3882
  const parentPreviousSibling = this.activityTree.getPreviousSibling(current.parent);
3795
3883
  if (parentPreviousSibling) {
3796
- return {
3797
- activity: this.getLastDescendant(parentPreviousSibling),
3798
- endSequencingSession: false
3799
- };
3884
+ return this.getBackwardTraversalEntry(parentPreviousSibling);
3800
3885
  }
3801
3886
  current = current.parent;
3802
3887
  }
3803
3888
  return { activity: null, endSequencingSession: false };
3804
3889
  }
3805
3890
  /**
3806
- * Get the last descendant of an activity
3891
+ * Get the activity entered by backward traversal.
3807
3892
  * @param {Activity} activity - The activity
3808
- * @return {Activity} - The last descendant
3893
+ * @return {FlowTreeTraversalResult} - The entered activity and effective direction
3894
+ * @spec SN Book: SB.2.1 (Flow Tree Traversal Subprocess) - entering a forwardOnly cluster while moving Backward uses the first available child and changes direction to Forward.
3809
3895
  */
3810
- getLastDescendant(activity) {
3811
- let lastDescendant = activity;
3896
+ getBackwardTraversalEntry(activity) {
3897
+ let enteredActivity = activity;
3812
3898
  let iterations = 0;
3813
3899
  const maxIterations = 1e4;
3814
3900
  while (true) {
3815
3901
  if (++iterations > maxIterations) {
3816
- throw new Error("Infinite loop detected while getting last descendant");
3902
+ throw new Error("Infinite loop detected while getting backward traversal entry");
3817
3903
  }
3818
- this.ensureSelectionAndRandomization(lastDescendant);
3819
- const children = lastDescendant.getAvailableChildren();
3904
+ this.ensureSelectionAndRandomization(enteredActivity);
3905
+ const children = enteredActivity.getAvailableChildren();
3820
3906
  if (children.length === 0) {
3821
3907
  break;
3822
3908
  }
3909
+ if (enteredActivity.sequencingControls.forwardOnly) {
3910
+ return {
3911
+ activity: children[0] || null,
3912
+ endSequencingSession: false,
3913
+ direction: FlowSubprocessMode.FORWARD,
3914
+ forwardOnlyCluster: enteredActivity
3915
+ };
3916
+ }
3823
3917
  const lastChild = children[children.length - 1];
3824
3918
  if (!lastChild) break;
3825
- lastDescendant = lastChild;
3919
+ enteredActivity = lastChild;
3920
+ }
3921
+ return {
3922
+ activity: enteredActivity,
3923
+ endSequencingSession: false
3924
+ };
3925
+ }
3926
+ /**
3927
+ * Check whether an activity is the same as or beneath an ancestor.
3928
+ * @param {Activity} activity - The activity to check
3929
+ * @param {Activity} ancestor - The expected ancestor
3930
+ * @return {boolean} - True when activity is within ancestor
3931
+ * @spec SN Book: SB.2.1 (Flow Tree Traversal Subprocess) - bounds Forward traversal after a forwardOnly direction reversal to the entered cluster.
3932
+ */
3933
+ isDescendantOfOrSelf(activity, ancestor) {
3934
+ let current = activity;
3935
+ while (current) {
3936
+ if (current === ancestor) {
3937
+ return true;
3938
+ }
3939
+ current = current.parent;
3826
3940
  }
3827
- return lastDescendant;
3941
+ return false;
3828
3942
  }
3829
3943
  /**
3830
3944
  * Flow Activity Traversal Subprocess (SB.2.2)
@@ -5003,7 +5117,9 @@ class AsynchronousHttpService {
5003
5117
  */
5004
5118
  async performBeacon(url, params) {
5005
5119
  const { body, contentType } = this._prepareRequestBody(params);
5006
- const beaconSuccess = navigator.sendBeacon(url, new Blob([body], { type: contentType }));
5120
+ const beaconContentType = Array.isArray(params) ? contentType : this.settings.terminationCommitContentType;
5121
+ this._warnIfBeaconContentTypeUnsafe(url, beaconContentType);
5122
+ const beaconSuccess = navigator.sendBeacon(url, new Blob([body], { type: beaconContentType }));
5007
5123
  return Promise.resolve({
5008
5124
  status: beaconSuccess ? 200 : 0,
5009
5125
  ok: beaconSuccess,
@@ -5017,6 +5133,14 @@ class AsynchronousHttpService {
5017
5133
  })
5018
5134
  });
5019
5135
  }
5136
+ _warnIfBeaconContentTypeUnsafe(url, contentType) {
5137
+ if (isCrossOriginUrl(url) && !isCorsSafelistedContentType(contentType)) {
5138
+ this.settings.onLogMessage?.(
5139
+ LogLevelEnum.WARN,
5140
+ `sendBeacon to cross-origin URL with non-CORS-safelisted Content-Type "${contentType}" may be silently dropped by the browser (Beacon cannot preflight). Use fetch (useAsynchronousCommits + asyncModeBeaconBehavior:"never") for cross-origin JSON/auth on terminate.`
5141
+ );
5142
+ }
5143
+ }
5020
5144
  /**
5021
5145
  * Transforms the response from the LMS to a ResultObject
5022
5146
  * @param {Response} response - The response from the LMS
@@ -5684,14 +5808,14 @@ class ErrorHandlingService {
5684
5808
  * @param {string} message - The error message
5685
5809
  * @throws {ValidationError} - If throwException is true, throws a ValidationError
5686
5810
  */
5687
- throwSCORMError(CMIElement, errorNumber, message) {
5811
+ throwSCORMError(CMIElement, errorNumber, message, messageLevel = LogLevelEnum.ERROR) {
5688
5812
  this._lastDiagnostic = message || "";
5689
5813
  if (!message) {
5690
5814
  message = this._getLmsErrorMessageDetails(errorNumber, true);
5691
5815
  }
5692
5816
  const formattedMessage = `SCORM Error ${errorNumber}: ${message}${CMIElement ? ` [Element: ${CMIElement}]` : ""}`;
5693
- this._apiLog("throwSCORMError", errorNumber + ": " + message, LogLevelEnum.ERROR, CMIElement);
5694
- this._loggingService.error(formattedMessage);
5817
+ this._apiLog("throwSCORMError", errorNumber + ": " + message, messageLevel, CMIElement);
5818
+ this._loggingService.log(messageLevel, formattedMessage);
5695
5819
  this._lastErrorCode = String(errorNumber);
5696
5820
  }
5697
5821
  /**
@@ -6374,7 +6498,9 @@ const checkValidFormat = memoize(
6374
6498
  (CMIElement, value, regexPattern, errorCode, _errorClass, allowEmptyString) => {
6375
6499
  const valueKey = typeof value === "string" ? value : `[${typeof value}]`;
6376
6500
  return `${CMIElement}:${valueKey}:${regexPattern}:${errorCode}:${allowEmptyString || false}`;
6377
- }
6501
+ },
6502
+ // Normal capped CMI values and regexes fit within 2000 characters; large uncapped values bypass caching.
6503
+ { maxEntries: 1e3, maxKeyLength: 2e3 }
6378
6504
  );
6379
6505
  const checkValidRange = memoize(
6380
6506
  (CMIElement, value, rangePattern, errorCode, errorClass) => {
@@ -6397,7 +6523,8 @@ const checkValidRange = memoize(
6397
6523
  },
6398
6524
  // Custom key function that excludes the error class from the cache key
6399
6525
  // since it can't be stringified and doesn't affect the validation result
6400
- (CMIElement, value, rangePattern, errorCode, _errorClass) => `${CMIElement}:${value}:${rangePattern}:${errorCode}`
6526
+ (CMIElement, value, rangePattern, errorCode, _errorClass) => `${CMIElement}:${value}:${rangePattern}:${errorCode}`,
6527
+ { maxEntries: 1e3 }
6401
6528
  );
6402
6529
 
6403
6530
  function check2004ValidFormat(CMIElement, value, regexPattern, allowEmptyString) {
@@ -6906,6 +7033,12 @@ class ActivityObjective {
6906
7033
  // objectives. It serves as a validity gate for other synced properties.
6907
7034
  _measureStatus = false;
6908
7035
  _normalizedMeasure = 0;
7036
+ _rawScore = "";
7037
+ _rawScoreKnown = false;
7038
+ _minScore = "";
7039
+ _minScoreKnown = false;
7040
+ _maxScore = "";
7041
+ _maxScoreKnown = false;
6909
7042
  _progressMeasure = 0;
6910
7043
  _progressMeasureStatus = false;
6911
7044
  _completionStatus = CompletionStatus.UNKNOWN;
@@ -6915,6 +7048,9 @@ class ActivityObjective {
6915
7048
  _normalizedMeasureDirty = false;
6916
7049
  _completionStatusDirty = false;
6917
7050
  _progressMeasureDirty = false;
7051
+ _rawScoreDirty = false;
7052
+ _minScoreDirty = false;
7053
+ _maxScoreDirty = false;
6918
7054
  constructor(id, options = {}) {
6919
7055
  this._id = id;
6920
7056
  this._description = options.description ?? null;
@@ -6960,6 +7096,8 @@ class ActivityObjective {
6960
7096
  if (this._satisfiedStatus !== value) {
6961
7097
  this._satisfiedStatus = value;
6962
7098
  this._satisfiedStatusDirty = true;
7099
+ this._satisfiedStatusKnown = true;
7100
+ this._progressStatus = true;
6963
7101
  }
6964
7102
  }
6965
7103
  get satisfiedStatusKnown() {
@@ -6983,6 +7121,114 @@ class ActivityObjective {
6983
7121
  this._normalizedMeasureDirty = true;
6984
7122
  }
6985
7123
  }
7124
+ /**
7125
+ * Return the known raw score value held for ADLSEQ objective score mapping.
7126
+ *
7127
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw score mapInfo
7128
+ */
7129
+ get rawScore() {
7130
+ return this._rawScore;
7131
+ }
7132
+ /**
7133
+ * Store the RTE raw score associated with this objective.
7134
+ *
7135
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - raw score mapInfo
7136
+ */
7137
+ set rawScore(value) {
7138
+ if (this._rawScore !== value) {
7139
+ this._rawScore = value;
7140
+ this._rawScoreDirty = true;
7141
+ }
7142
+ this._rawScoreKnown = value !== "";
7143
+ }
7144
+ /**
7145
+ * Return whether this objective's raw score is known.
7146
+ *
7147
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw score known state
7148
+ */
7149
+ get rawScoreKnown() {
7150
+ return this._rawScoreKnown;
7151
+ }
7152
+ /**
7153
+ * Set whether this objective's raw score is known.
7154
+ *
7155
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw score known state
7156
+ */
7157
+ set rawScoreKnown(value) {
7158
+ this._rawScoreKnown = value;
7159
+ }
7160
+ /**
7161
+ * Return the known minimum score value held for ADLSEQ objective score mapping.
7162
+ *
7163
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - min score mapInfo
7164
+ */
7165
+ get minScore() {
7166
+ return this._minScore;
7167
+ }
7168
+ /**
7169
+ * Store the RTE minimum score associated with this objective.
7170
+ *
7171
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - min score mapInfo
7172
+ */
7173
+ set minScore(value) {
7174
+ if (this._minScore !== value) {
7175
+ this._minScore = value;
7176
+ this._minScoreDirty = true;
7177
+ }
7178
+ this._minScoreKnown = value !== "";
7179
+ }
7180
+ /**
7181
+ * Return whether this objective's minimum score is known.
7182
+ *
7183
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - min score known state
7184
+ */
7185
+ get minScoreKnown() {
7186
+ return this._minScoreKnown;
7187
+ }
7188
+ /**
7189
+ * Set whether this objective's minimum score is known.
7190
+ *
7191
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - min score known state
7192
+ */
7193
+ set minScoreKnown(value) {
7194
+ this._minScoreKnown = value;
7195
+ }
7196
+ /**
7197
+ * Return the known maximum score value held for ADLSEQ objective score mapping.
7198
+ *
7199
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - max score mapInfo
7200
+ */
7201
+ get maxScore() {
7202
+ return this._maxScore;
7203
+ }
7204
+ /**
7205
+ * Store the RTE maximum score associated with this objective.
7206
+ *
7207
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - max score mapInfo
7208
+ */
7209
+ set maxScore(value) {
7210
+ if (this._maxScore !== value) {
7211
+ this._maxScore = value;
7212
+ this._maxScoreDirty = true;
7213
+ }
7214
+ this._maxScoreKnown = value !== "";
7215
+ }
7216
+ /**
7217
+ * Return whether this objective's maximum score is known.
7218
+ *
7219
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - max score known state
7220
+ */
7221
+ get maxScoreKnown() {
7222
+ return this._maxScoreKnown;
7223
+ }
7224
+ /**
7225
+ * Set whether this objective's maximum score is known.
7226
+ *
7227
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - max score known state
7228
+ */
7229
+ set maxScoreKnown(value) {
7230
+ this._maxScoreKnown = value;
7231
+ }
6986
7232
  get progressMeasure() {
6987
7233
  return this._progressMeasure;
6988
7234
  }
@@ -7013,6 +7259,11 @@ class ActivityObjective {
7013
7259
  set progressStatus(value) {
7014
7260
  this._progressStatus = value;
7015
7261
  }
7262
+ /**
7263
+ * Report whether a local objective field has changed since the last global write.
7264
+ *
7265
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - write maps use known local objective data
7266
+ */
7016
7267
  isDirty(property) {
7017
7268
  switch (property) {
7018
7269
  case "satisfiedStatus":
@@ -7023,8 +7274,19 @@ class ActivityObjective {
7023
7274
  return this._completionStatusDirty;
7024
7275
  case "progressMeasure":
7025
7276
  return this._progressMeasureDirty;
7277
+ case "rawScore":
7278
+ return this._rawScoreDirty;
7279
+ case "minScore":
7280
+ return this._minScoreDirty;
7281
+ case "maxScore":
7282
+ return this._maxScoreDirty;
7026
7283
  }
7027
7284
  }
7285
+ /**
7286
+ * Clear a local objective dirty flag after a successful global write.
7287
+ *
7288
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - write maps update global objective state
7289
+ */
7028
7290
  clearDirty(property) {
7029
7291
  switch (property) {
7030
7292
  case "satisfiedStatus":
@@ -7039,13 +7301,31 @@ class ActivityObjective {
7039
7301
  case "progressMeasure":
7040
7302
  this._progressMeasureDirty = false;
7041
7303
  break;
7304
+ case "rawScore":
7305
+ this._rawScoreDirty = false;
7306
+ break;
7307
+ case "minScore":
7308
+ this._minScoreDirty = false;
7309
+ break;
7310
+ case "maxScore":
7311
+ this._maxScoreDirty = false;
7312
+ break;
7042
7313
  }
7043
7314
  }
7315
+ /**
7316
+ * Clear all write-map dirty flags for this objective.
7317
+ *
7318
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - objective mapInfo writes are field-specific
7319
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score mapInfo writes are field-specific
7320
+ */
7044
7321
  clearAllDirty() {
7045
7322
  this._satisfiedStatusDirty = false;
7046
7323
  this._normalizedMeasureDirty = false;
7047
7324
  this._completionStatusDirty = false;
7048
7325
  this._progressMeasureDirty = false;
7326
+ this._rawScoreDirty = false;
7327
+ this._minScoreDirty = false;
7328
+ this._maxScoreDirty = false;
7049
7329
  }
7050
7330
  /**
7051
7331
  * Initialize objective values from CMI data transfer
@@ -7055,6 +7335,8 @@ class ActivityObjective {
7055
7335
  * @param satisfiedStatus - The satisfied status from CMI
7056
7336
  * @param normalizedMeasure - The normalized measure from CMI
7057
7337
  * @param measureStatus - Whether measure is valid
7338
+ *
7339
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - objective satisfaction and measure transfer
7058
7340
  */
7059
7341
  initializeFromCMI(satisfiedStatus, normalizedMeasure, measureStatus) {
7060
7342
  this._satisfiedStatus = satisfiedStatus;
@@ -7063,17 +7345,93 @@ class ActivityObjective {
7063
7345
  this._normalizedMeasureDirty = true;
7064
7346
  this._measureStatus = measureStatus;
7065
7347
  }
7348
+ /**
7349
+ * Initialize raw/min/max objective score values from RTE data transfer.
7350
+ *
7351
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - objective score transfer
7352
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score mapInfo writes
7353
+ */
7354
+ initializeScoreFromCMI(score) {
7355
+ if (score.rawScore !== void 0 && score.rawScore !== "") {
7356
+ this._rawScore = score.rawScore;
7357
+ this._rawScoreKnown = true;
7358
+ this._rawScoreDirty = true;
7359
+ }
7360
+ if (score.minScore !== void 0 && score.minScore !== "") {
7361
+ this._minScore = score.minScore;
7362
+ this._minScoreKnown = true;
7363
+ this._minScoreDirty = true;
7364
+ }
7365
+ if (score.maxScore !== void 0 && score.maxScore !== "") {
7366
+ this._maxScore = score.maxScore;
7367
+ this._maxScoreKnown = true;
7368
+ this._maxScoreDirty = true;
7369
+ }
7370
+ }
7371
+ /**
7372
+ * Apply read-mapped global objective state without marking the values dirty.
7373
+ *
7374
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read maps provide access to global objective state
7375
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score read maps are access-only
7376
+ */
7377
+ applyReadMappedState(state) {
7378
+ if (state.satisfiedStatus !== void 0) {
7379
+ this._satisfiedStatus = state.satisfiedStatus;
7380
+ this._satisfiedStatusKnown = true;
7381
+ this._progressStatus = true;
7382
+ }
7383
+ if (state.normalizedMeasure !== void 0) {
7384
+ this._normalizedMeasure = state.normalizedMeasure;
7385
+ this._measureStatus = true;
7386
+ }
7387
+ if (state.completionStatus !== void 0) {
7388
+ this._completionStatus = state.completionStatus;
7389
+ }
7390
+ if (state.progressMeasure !== void 0) {
7391
+ this._progressMeasure = state.progressMeasure;
7392
+ this._progressMeasureStatus = true;
7393
+ }
7394
+ if (state.rawScore !== void 0) {
7395
+ this._rawScore = state.rawScore;
7396
+ this._rawScoreKnown = true;
7397
+ }
7398
+ if (state.minScore !== void 0) {
7399
+ this._minScore = state.minScore;
7400
+ this._minScoreKnown = true;
7401
+ }
7402
+ if (state.maxScore !== void 0) {
7403
+ this._maxScore = state.maxScore;
7404
+ this._maxScoreKnown = true;
7405
+ }
7406
+ }
7407
+ /**
7408
+ * Reset local objective state for a fresh activity attempt.
7409
+ *
7410
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - unknown objective state before tracking data exists
7411
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score map fields are unknown until transferred or read
7412
+ */
7066
7413
  resetState() {
7067
7414
  this._satisfiedStatus = false;
7068
7415
  this._satisfiedStatusKnown = false;
7069
7416
  this._measureStatus = false;
7070
7417
  this._normalizedMeasure = 0;
7418
+ this._rawScore = "";
7419
+ this._rawScoreKnown = false;
7420
+ this._minScore = "";
7421
+ this._minScoreKnown = false;
7422
+ this._maxScore = "";
7423
+ this._maxScoreKnown = false;
7071
7424
  this._progressMeasure = 0;
7072
7425
  this._progressMeasureStatus = false;
7073
7426
  this._completionStatus = CompletionStatus.UNKNOWN;
7074
7427
  this._progressStatus = false;
7075
7428
  this.clearAllDirty();
7076
7429
  }
7430
+ /**
7431
+ * Copy primary activity objective state back into the primary objective model.
7432
+ *
7433
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - primary objective state is available to sequencing
7434
+ */
7077
7435
  updateFromActivity(activity) {
7078
7436
  if (this._satisfiedStatus !== activity.objectiveSatisfiedStatus) {
7079
7437
  this._satisfiedStatus = activity.objectiveSatisfiedStatus;
@@ -7095,6 +7453,11 @@ class ActivityObjective {
7095
7453
  this._completionStatusDirty = true;
7096
7454
  }
7097
7455
  }
7456
+ /**
7457
+ * Apply primary objective state to the owning activity for sequencing rules and rollup.
7458
+ *
7459
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - primary objective contributes activity state
7460
+ */
7098
7461
  applyToActivity(activity) {
7099
7462
  if (!this._isPrimary) {
7100
7463
  return;
@@ -7105,7 +7468,8 @@ class ActivityObjective {
7105
7468
  this._normalizedMeasure,
7106
7469
  this._progressMeasure,
7107
7470
  this._progressMeasureStatus,
7108
- this._completionStatus
7471
+ this._completionStatus,
7472
+ this._progressStatus || this._satisfiedStatusKnown
7109
7473
  );
7110
7474
  }
7111
7475
  }
@@ -8175,7 +8539,7 @@ class Activity extends BaseCMI {
8175
8539
  /**
8176
8540
  * Setter for primary objective
8177
8541
  * @param {ActivityObjective | null} objective
8178
- */
8542
+ */
8179
8543
  set primaryObjective(objective) {
8180
8544
  this._primaryObjective = objective;
8181
8545
  if (this._primaryObjective) {
@@ -8197,7 +8561,7 @@ class Activity extends BaseCMI {
8197
8561
  /**
8198
8562
  * Replace objectives collection
8199
8563
  * @param {ActivityObjective[]} objectives
8200
- */
8564
+ */
8201
8565
  set objectives(objectives) {
8202
8566
  this._objectives = [...objectives];
8203
8567
  this.syncPrimaryObjectiveCollection();
@@ -8290,12 +8654,12 @@ class Activity extends BaseCMI {
8290
8654
  this._objectiveNormalizedMeasureDirty = false;
8291
8655
  this._objectiveMeasureStatusDirty = false;
8292
8656
  }
8293
- setPrimaryObjectiveState(satisfiedStatus, measureStatus, normalizedMeasure, progressMeasure, progressMeasureStatus, completionStatus) {
8657
+ setPrimaryObjectiveState(satisfiedStatus, measureStatus, normalizedMeasure, progressMeasure, progressMeasureStatus, completionStatus, objectiveProgressStatus = true) {
8294
8658
  if (this._objectiveSatisfiedStatus !== satisfiedStatus) {
8295
8659
  this._objectiveSatisfiedStatus = satisfiedStatus;
8296
8660
  this._objectiveSatisfiedStatusDirty = true;
8297
8661
  }
8298
- this._objectiveSatisfiedStatusKnown = true;
8662
+ this._objectiveSatisfiedStatusKnown = objectiveProgressStatus;
8299
8663
  if (this._objectiveMeasureStatus !== measureStatus) {
8300
8664
  this._objectiveMeasureStatus = measureStatus;
8301
8665
  this._objectiveMeasureStatusDirty = true;
@@ -8314,14 +8678,28 @@ class Activity extends BaseCMI {
8314
8678
  this._primaryObjective.progressMeasure = progressMeasure;
8315
8679
  this._primaryObjective.progressMeasureStatus = progressMeasureStatus;
8316
8680
  this._primaryObjective.completionStatus = completionStatus;
8681
+ this._primaryObjective.satisfiedStatusKnown = objectiveProgressStatus;
8682
+ this._primaryObjective.progressStatus = objectiveProgressStatus;
8317
8683
  }
8318
8684
  }
8685
+ /**
8686
+ * Snapshot objective state for sequencing persistence.
8687
+ *
8688
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - objective state persists across attempts
8689
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score-map state persists with objective state
8690
+ */
8319
8691
  getObjectiveStateSnapshot() {
8320
8692
  const primarySnapshot = this._primaryObjective ? {
8321
8693
  id: this._primaryObjective.id,
8322
8694
  satisfiedStatus: this.objectiveSatisfiedStatus,
8323
8695
  measureStatus: this.objectiveMeasureStatus,
8324
8696
  normalizedMeasure: this.objectiveNormalizedMeasure,
8697
+ rawScore: this._primaryObjective.rawScore,
8698
+ rawScoreKnown: this._primaryObjective.rawScoreKnown,
8699
+ minScore: this._primaryObjective.minScore,
8700
+ minScoreKnown: this._primaryObjective.minScoreKnown,
8701
+ maxScore: this._primaryObjective.maxScore,
8702
+ maxScoreKnown: this._primaryObjective.maxScoreKnown,
8325
8703
  progressMeasure: this.progressMeasure ?? 0,
8326
8704
  progressMeasureStatus: this.progressMeasureStatus,
8327
8705
  progressStatus: this._primaryObjective.progressStatus,
@@ -8334,6 +8712,12 @@ class Activity extends BaseCMI {
8334
8712
  satisfiedStatus: objective.satisfiedStatus,
8335
8713
  measureStatus: objective.measureStatus,
8336
8714
  normalizedMeasure: objective.normalizedMeasure,
8715
+ rawScore: objective.rawScore,
8716
+ rawScoreKnown: objective.rawScoreKnown,
8717
+ minScore: objective.minScore,
8718
+ minScoreKnown: objective.minScoreKnown,
8719
+ maxScore: objective.maxScore,
8720
+ maxScoreKnown: objective.maxScoreKnown,
8337
8721
  progressMeasure: objective.progressMeasure,
8338
8722
  progressMeasureStatus: objective.progressMeasureStatus,
8339
8723
  progressStatus: objective.progressStatus,
@@ -8346,6 +8730,12 @@ class Activity extends BaseCMI {
8346
8730
  objectives: additionalSnapshots
8347
8731
  };
8348
8732
  }
8733
+ /**
8734
+ * Restore objective state from a sequencing persistence snapshot.
8735
+ *
8736
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - persisted objective state restores sequencing state
8737
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - persisted score-map state restores objective score fields
8738
+ */
8349
8739
  applyObjectiveStateSnapshot(snapshot) {
8350
8740
  if (snapshot.primary) {
8351
8741
  const primary = this.getObjectiveById(snapshot.primary.id);
@@ -8359,8 +8749,10 @@ class Activity extends BaseCMI {
8359
8749
  state.normalizedMeasure,
8360
8750
  state.progressMeasure,
8361
8751
  state.progressMeasureStatus,
8362
- state.completionStatus
8752
+ state.completionStatus,
8753
+ state.progressStatus
8363
8754
  );
8755
+ this.applyObjectiveScoreSnapshot(primary.objective, state);
8364
8756
  }
8365
8757
  }
8366
8758
  for (const state of snapshot.objectives) {
@@ -8373,11 +8765,30 @@ class Activity extends BaseCMI {
8373
8765
  objective.progressMeasure = state.progressMeasure;
8374
8766
  objective.progressMeasureStatus = state.progressMeasureStatus;
8375
8767
  objective.completionStatus = state.completionStatus;
8768
+ this.applyObjectiveScoreSnapshot(objective, state);
8376
8769
  objective.satisfiedByMeasure = state.satisfiedByMeasure ?? objective.satisfiedByMeasure;
8377
8770
  objective.minNormalizedMeasure = state.minNormalizedMeasure !== void 0 ? state.minNormalizedMeasure : objective.minNormalizedMeasure;
8378
8771
  }
8379
8772
  }
8380
8773
  }
8774
+ /**
8775
+ * Restore known raw/min/max score fields from an objective state snapshot.
8776
+ *
8777
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score known flags restore independently
8778
+ */
8779
+ applyObjectiveScoreSnapshot(objective, state) {
8780
+ const scoreState = {};
8781
+ if (state.rawScoreKnown) {
8782
+ scoreState.rawScore = state.rawScore;
8783
+ }
8784
+ if (state.minScoreKnown) {
8785
+ scoreState.minScore = state.minScore;
8786
+ }
8787
+ if (state.maxScoreKnown) {
8788
+ scoreState.maxScore = state.maxScore;
8789
+ }
8790
+ objective.applyReadMappedState(scoreState);
8791
+ }
8381
8792
  /**
8382
8793
  * Get available children with selection and randomization applied
8383
8794
  * @return {Activity[]}
@@ -9768,11 +10179,18 @@ class GlobalObjectiveSynchronizer {
9768
10179
  *
9769
10180
  * @param activity - The activity to process
9770
10181
  * @param globalObjectives - Global objective map
10182
+ *
10183
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - write mapInfo transfers local objective state to global objectives
10184
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score write maps
9771
10185
  */
9772
10186
  syncGlobalObjectivesWritePhase(activity, globalObjectives) {
10187
+ if (!this.canWriteGlobalObjectives(activity)) {
10188
+ return;
10189
+ }
9773
10190
  const objectives = activity.getAllObjectives();
9774
10191
  for (const objective of objectives) {
9775
10192
  const mapInfos = objective.mapInfo.length > 0 ? objective.mapInfo : [this.createDefaultMapInfo(objective)];
10193
+ const dirtyFieldsToClear = /* @__PURE__ */ new Set();
9776
10194
  for (const mapInfo of mapInfos) {
9777
10195
  const targetId = mapInfo.targetObjectiveID || objective.id;
9778
10196
  const globalObjective = this.ensureGlobalObjectiveEntry(
@@ -9780,36 +10198,54 @@ class GlobalObjectiveSynchronizer {
9780
10198
  targetId,
9781
10199
  objective
9782
10200
  );
9783
- if (mapInfo.writeSatisfiedStatus && objective.measureStatus && objective.isDirty("satisfiedStatus")) {
10201
+ if (mapInfo.writeSatisfiedStatus && this.hasKnownSatisfiedStatus(objective) && objective.isDirty("satisfiedStatus")) {
9784
10202
  globalObjective.satisfiedStatus = objective.satisfiedStatus;
9785
10203
  globalObjective.satisfiedStatusKnown = true;
9786
- objective.clearDirty("satisfiedStatus");
10204
+ dirtyFieldsToClear.add("satisfiedStatus");
9787
10205
  }
9788
10206
  if (mapInfo.writeNormalizedMeasure && objective.measureStatus && objective.isDirty("normalizedMeasure")) {
9789
10207
  globalObjective.normalizedMeasure = objective.normalizedMeasure;
9790
10208
  globalObjective.normalizedMeasureKnown = true;
9791
- objective.clearDirty("normalizedMeasure");
9792
- if (globalObjective.satisfiedByMeasure || objective.satisfiedByMeasure) {
10209
+ dirtyFieldsToClear.add("normalizedMeasure");
10210
+ if (mapInfo.writeSatisfiedStatus && objective.satisfiedByMeasure) {
9793
10211
  const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
9794
10212
  globalObjective.satisfiedStatus = objective.normalizedMeasure >= threshold;
9795
10213
  globalObjective.satisfiedStatusKnown = true;
9796
- objective.clearDirty("satisfiedStatus");
10214
+ dirtyFieldsToClear.add("satisfiedStatus");
9797
10215
  }
9798
10216
  }
9799
10217
  if (mapInfo.writeCompletionStatus && objective.completionStatus !== CompletionStatus.UNKNOWN && objective.isDirty("completionStatus")) {
9800
10218
  globalObjective.completionStatus = objective.completionStatus;
9801
10219
  globalObjective.completionStatusKnown = true;
9802
- objective.clearDirty("completionStatus");
10220
+ dirtyFieldsToClear.add("completionStatus");
10221
+ }
10222
+ if (mapInfo.writeRawScore && objective.rawScoreKnown && objective.isDirty("rawScore")) {
10223
+ globalObjective.rawScore = objective.rawScore;
10224
+ globalObjective.rawScoreKnown = true;
10225
+ dirtyFieldsToClear.add("rawScore");
10226
+ }
10227
+ if (mapInfo.writeMinScore && objective.minScoreKnown && objective.isDirty("minScore")) {
10228
+ globalObjective.minScore = objective.minScore;
10229
+ globalObjective.minScoreKnown = true;
10230
+ dirtyFieldsToClear.add("minScore");
10231
+ }
10232
+ if (mapInfo.writeMaxScore && objective.maxScoreKnown && objective.isDirty("maxScore")) {
10233
+ globalObjective.maxScore = objective.maxScore;
10234
+ globalObjective.maxScoreKnown = true;
10235
+ dirtyFieldsToClear.add("maxScore");
9803
10236
  }
9804
10237
  if (mapInfo.writeProgressMeasure && objective.progressMeasureStatus && objective.isDirty("progressMeasure")) {
9805
10238
  globalObjective.progressMeasure = objective.progressMeasure;
9806
10239
  globalObjective.progressMeasureKnown = true;
9807
- objective.clearDirty("progressMeasure");
10240
+ dirtyFieldsToClear.add("progressMeasure");
9808
10241
  }
9809
10242
  if (mapInfo.updateAttemptData) {
9810
10243
  this.updateActivityAttemptData(activity, globalObjective, objective);
9811
10244
  }
9812
10245
  }
10246
+ for (const property of dirtyFieldsToClear) {
10247
+ objective.clearDirty(property);
10248
+ }
9813
10249
  }
9814
10250
  }
9815
10251
  /**
@@ -9817,6 +10253,9 @@ class GlobalObjectiveSynchronizer {
9817
10253
  *
9818
10254
  * @param activity - The activity to process
9819
10255
  * @param globalObjectives - Global objective map
10256
+ *
10257
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read mapInfo transfers global objective state into the local view
10258
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score read maps
9820
10259
  */
9821
10260
  syncGlobalObjectivesReadPhase(activity, globalObjectives) {
9822
10261
  const objectives = activity.getAllObjectives();
@@ -9827,25 +10266,13 @@ class GlobalObjectiveSynchronizer {
9827
10266
  const globalObjective = globalObjectives.get(targetId);
9828
10267
  if (!globalObjective) continue;
9829
10268
  const isPrimary = objective.isPrimary;
9830
- if (mapInfo.readSatisfiedStatus && globalObjective.satisfiedStatusKnown) {
9831
- objective.satisfiedStatus = globalObjective.satisfiedStatus;
9832
- objective.measureStatus = true;
9833
- }
9834
- if (mapInfo.readNormalizedMeasure && globalObjective.normalizedMeasureKnown) {
9835
- objective.normalizedMeasure = globalObjective.normalizedMeasure;
9836
- objective.measureStatus = true;
9837
- if (globalObjective.satisfiedByMeasure || objective.satisfiedByMeasure) {
9838
- const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
9839
- objective.satisfiedStatus = globalObjective.normalizedMeasure >= threshold;
9840
- }
9841
- }
9842
- if (mapInfo.readProgressMeasure && globalObjective.progressMeasureKnown) {
9843
- objective.progressMeasure = globalObjective.progressMeasure;
9844
- objective.progressMeasureStatus = true;
9845
- }
9846
- if (mapInfo.readCompletionStatus && globalObjective.completionStatusKnown) {
9847
- objective.completionStatus = globalObjective.completionStatus;
9848
- }
10269
+ const readState = GlobalObjectiveSynchronizer.getGlobalObjectiveReadState(
10270
+ activity,
10271
+ objective,
10272
+ mapInfo,
10273
+ globalObjective
10274
+ );
10275
+ this.applyGlobalObjectiveReadState(objective, readState);
9849
10276
  if (isPrimary) {
9850
10277
  objective.applyToActivity(activity);
9851
10278
  }
@@ -9888,56 +10315,61 @@ class GlobalObjectiveSynchronizer {
9888
10315
  * @param objective - The objective to sync
9889
10316
  * @param mapInfo - Map info for this objective
9890
10317
  * @param globalObjective - The global objective
10318
+ *
10319
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - objective mapInfo read/write synchronization
10320
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score mapInfo synchronization
9891
10321
  */
9892
10322
  syncObjectiveState(activity, objective, mapInfo, globalObjective) {
9893
10323
  try {
9894
10324
  const isPrimary = objective.isPrimary;
9895
10325
  const localObjective = this.getLocalObjectiveState(activity, objective, isPrimary);
9896
- if (mapInfo.readSatisfiedStatus && globalObjective.satisfiedStatusKnown) {
9897
- objective.satisfiedStatus = globalObjective.satisfiedStatus;
9898
- objective.measureStatus = true;
9899
- }
9900
- if (mapInfo.readNormalizedMeasure && globalObjective.normalizedMeasureKnown) {
9901
- objective.normalizedMeasure = globalObjective.normalizedMeasure;
9902
- objective.measureStatus = true;
9903
- if (globalObjective.satisfiedByMeasure || objective.satisfiedByMeasure) {
9904
- const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
9905
- objective.satisfiedStatus = globalObjective.normalizedMeasure >= threshold;
9906
- }
9907
- }
9908
- if (mapInfo.readProgressMeasure && globalObjective.progressMeasureKnown) {
9909
- objective.progressMeasure = globalObjective.progressMeasure;
9910
- objective.progressMeasureStatus = true;
9911
- }
9912
- if (mapInfo.readCompletionStatus && globalObjective.completionStatusKnown) {
9913
- objective.completionStatus = globalObjective.completionStatus;
9914
- }
10326
+ const readState = GlobalObjectiveSynchronizer.getGlobalObjectiveReadState(
10327
+ activity,
10328
+ objective,
10329
+ mapInfo,
10330
+ globalObjective
10331
+ );
10332
+ this.applyGlobalObjectiveReadState(objective, readState);
9915
10333
  if (objective.isPrimary) {
9916
10334
  objective.applyToActivity(activity);
9917
10335
  }
9918
- if (mapInfo.writeSatisfiedStatus && objective.measureStatus) {
9919
- globalObjective.satisfiedStatus = objective.satisfiedStatus;
9920
- globalObjective.satisfiedStatusKnown = true;
9921
- }
9922
- if (mapInfo.writeNormalizedMeasure && objective.measureStatus) {
9923
- globalObjective.normalizedMeasure = objective.normalizedMeasure;
9924
- globalObjective.normalizedMeasureKnown = true;
9925
- if (globalObjective.satisfiedByMeasure || objective.satisfiedByMeasure) {
9926
- const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
9927
- globalObjective.satisfiedStatus = objective.normalizedMeasure >= threshold;
10336
+ if (this.canWriteGlobalObjectives(activity)) {
10337
+ if (mapInfo.writeSatisfiedStatus && this.hasKnownSatisfiedStatus(objective)) {
10338
+ globalObjective.satisfiedStatus = objective.satisfiedStatus;
9928
10339
  globalObjective.satisfiedStatusKnown = true;
9929
10340
  }
9930
- }
9931
- if (mapInfo.writeCompletionStatus && objective.completionStatus !== CompletionStatus.UNKNOWN) {
9932
- globalObjective.completionStatus = objective.completionStatus;
9933
- globalObjective.completionStatusKnown = true;
9934
- }
9935
- if (mapInfo.writeProgressMeasure && objective.progressMeasureStatus) {
9936
- globalObjective.progressMeasure = objective.progressMeasure;
9937
- globalObjective.progressMeasureKnown = true;
9938
- }
9939
- if (mapInfo.updateAttemptData) {
9940
- this.updateActivityAttemptData(activity, globalObjective, objective);
10341
+ if (mapInfo.writeNormalizedMeasure && objective.measureStatus) {
10342
+ globalObjective.normalizedMeasure = objective.normalizedMeasure;
10343
+ globalObjective.normalizedMeasureKnown = true;
10344
+ if (mapInfo.writeSatisfiedStatus && objective.satisfiedByMeasure) {
10345
+ const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
10346
+ globalObjective.satisfiedStatus = objective.normalizedMeasure >= threshold;
10347
+ globalObjective.satisfiedStatusKnown = true;
10348
+ }
10349
+ }
10350
+ if (mapInfo.writeCompletionStatus && objective.completionStatus !== CompletionStatus.UNKNOWN) {
10351
+ globalObjective.completionStatus = objective.completionStatus;
10352
+ globalObjective.completionStatusKnown = true;
10353
+ }
10354
+ if (mapInfo.writeRawScore && objective.rawScoreKnown) {
10355
+ globalObjective.rawScore = objective.rawScore;
10356
+ globalObjective.rawScoreKnown = true;
10357
+ }
10358
+ if (mapInfo.writeMinScore && objective.minScoreKnown) {
10359
+ globalObjective.minScore = objective.minScore;
10360
+ globalObjective.minScoreKnown = true;
10361
+ }
10362
+ if (mapInfo.writeMaxScore && objective.maxScoreKnown) {
10363
+ globalObjective.maxScore = objective.maxScore;
10364
+ globalObjective.maxScoreKnown = true;
10365
+ }
10366
+ if (mapInfo.writeProgressMeasure && objective.progressMeasureStatus) {
10367
+ globalObjective.progressMeasure = objective.progressMeasure;
10368
+ globalObjective.progressMeasureKnown = true;
10369
+ }
10370
+ if (mapInfo.updateAttemptData) {
10371
+ this.updateActivityAttemptData(activity, globalObjective, objective);
10372
+ }
9941
10373
  }
9942
10374
  this.eventCallback?.("objective_synchronized", {
9943
10375
  activityId: activity.id,
@@ -9955,6 +10387,50 @@ class GlobalObjectiveSynchronizer {
9955
10387
  });
9956
10388
  }
9957
10389
  }
10390
+ /**
10391
+ * Project a global objective through one local objective's read mapInfo.
10392
+ *
10393
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read maps provide access to mapped global objective fields
10394
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score read maps are independent fields
10395
+ */
10396
+ static getGlobalObjectiveReadState(activity, objective, mapInfo, globalObjective) {
10397
+ const readState = {};
10398
+ if (mapInfo.readSatisfiedStatus && globalObjective.satisfiedStatusKnown) {
10399
+ readState.satisfiedStatus = globalObjective.satisfiedStatus;
10400
+ }
10401
+ if (mapInfo.readNormalizedMeasure && globalObjective.normalizedMeasureKnown) {
10402
+ readState.normalizedMeasure = globalObjective.normalizedMeasure;
10403
+ if (objective.satisfiedByMeasure) {
10404
+ const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
10405
+ readState.satisfiedStatus = globalObjective.normalizedMeasure >= threshold;
10406
+ }
10407
+ }
10408
+ if (mapInfo.readCompletionStatus && globalObjective.completionStatusKnown) {
10409
+ readState.completionStatus = globalObjective.completionStatus;
10410
+ }
10411
+ if (mapInfo.readProgressMeasure && globalObjective.progressMeasureKnown) {
10412
+ readState.progressMeasure = globalObjective.progressMeasure;
10413
+ }
10414
+ if (mapInfo.readRawScore && globalObjective.rawScoreKnown) {
10415
+ readState.rawScore = globalObjective.rawScore;
10416
+ }
10417
+ if (mapInfo.readMinScore && globalObjective.minScoreKnown) {
10418
+ readState.minScore = globalObjective.minScore;
10419
+ }
10420
+ if (mapInfo.readMaxScore && globalObjective.maxScoreKnown) {
10421
+ readState.maxScore = globalObjective.maxScore;
10422
+ }
10423
+ return readState;
10424
+ }
10425
+ /**
10426
+ * Apply read-mapped state to an objective without marking those fields dirty.
10427
+ *
10428
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read maps are access to global state, not local writes
10429
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score read maps do not imply score writes
10430
+ */
10431
+ applyGlobalObjectiveReadState(objective, readState) {
10432
+ objective.applyReadMappedState(readState);
10433
+ }
9958
10434
  /**
9959
10435
  * Ensure global objective entry exists
9960
10436
  *
@@ -9962,19 +10438,30 @@ class GlobalObjectiveSynchronizer {
9962
10438
  * @param targetId - Target objective ID
9963
10439
  * @param objective - Source objective
9964
10440
  * @returns The global objective entry
10441
+ *
10442
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objective entries hold mapped objective state
10443
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - global entries hold score-map state
9965
10444
  */
9966
10445
  ensureGlobalObjectiveEntry(globalObjectives, targetId, objective) {
9967
10446
  if (!globalObjectives.has(targetId)) {
9968
10447
  globalObjectives.set(targetId, {
9969
10448
  id: targetId,
9970
- satisfiedStatus: objective.satisfiedStatus,
9971
- satisfiedStatusKnown: objective.satisfiedStatusKnown,
9972
- normalizedMeasure: objective.normalizedMeasure,
9973
- normalizedMeasureKnown: objective.measureStatus,
9974
- progressMeasure: objective.progressMeasure,
9975
- progressMeasureKnown: objective.progressMeasureStatus,
9976
- completionStatus: objective.completionStatus,
9977
- completionStatusKnown: objective.completionStatus !== CompletionStatus.UNKNOWN,
10449
+ satisfiedStatus: false,
10450
+ satisfiedStatusKnown: false,
10451
+ normalizedMeasure: 0,
10452
+ normalizedMeasureKnown: false,
10453
+ // @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score fields
10454
+ // remain unknown until their corresponding write map explicitly writes them.
10455
+ rawScore: "",
10456
+ rawScoreKnown: false,
10457
+ minScore: "",
10458
+ minScoreKnown: false,
10459
+ maxScore: "",
10460
+ maxScoreKnown: false,
10461
+ progressMeasure: 0,
10462
+ progressMeasureKnown: false,
10463
+ completionStatus: CompletionStatus.UNKNOWN,
10464
+ completionStatusKnown: false,
9978
10465
  satisfiedByMeasure: objective.satisfiedByMeasure,
9979
10466
  minNormalizedMeasure: objective.minNormalizedMeasure
9980
10467
  });
@@ -9989,6 +10476,9 @@ class GlobalObjectiveSynchronizer {
9989
10476
  *
9990
10477
  * @param objective - The objective to create default map info for
9991
10478
  * @returns Default map info
10479
+ *
10480
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - mapInfo defaults are applied before objective synchronization
10481
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score maps require explicit read/write flags
9992
10482
  */
9993
10483
  createDefaultMapInfo(objective) {
9994
10484
  return {
@@ -10001,9 +10491,34 @@ class GlobalObjectiveSynchronizer {
10001
10491
  writeCompletionStatus: true,
10002
10492
  readProgressMeasure: false,
10003
10493
  writeProgressMeasure: true,
10494
+ readRawScore: false,
10495
+ writeRawScore: false,
10496
+ readMinScore: false,
10497
+ writeMinScore: false,
10498
+ readMaxScore: false,
10499
+ writeMaxScore: false,
10004
10500
  updateAttemptData: objective.isPrimary
10005
10501
  };
10006
10502
  }
10503
+ /**
10504
+ * Return whether the local objective has known satisfaction state to write.
10505
+ *
10506
+ * @spec SCORM 2004 4th Ed. SN 4.2.1 Tracking Model - Objective Progress
10507
+ * Status identifies whether Objective Satisfied Status is known; Objective
10508
+ * Measure Status is independent measure knowledge.
10509
+ */
10510
+ hasKnownSatisfiedStatus(objective) {
10511
+ return objective.progressStatus || objective.satisfiedStatusKnown;
10512
+ }
10513
+ /**
10514
+ * Return whether this activity is allowed to write tracked state to globals.
10515
+ *
10516
+ * @spec SCORM 2004 4th Ed. SN 3.13.1 Tracked - when False, the LMS
10517
+ * "does not initialize, manage or access any tracking status information".
10518
+ */
10519
+ canWriteGlobalObjectives(activity) {
10520
+ return activity.sequencingControls.tracked !== false;
10521
+ }
10007
10522
  /**
10008
10523
  * Get local objective state
10009
10524
  *
@@ -10011,6 +10526,9 @@ class GlobalObjectiveSynchronizer {
10011
10526
  * @param objective - The objective
10012
10527
  * @param isPrimary - Whether this is the primary objective
10013
10528
  * @returns Local objective state
10529
+ *
10530
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - local objective state used for synchronization
10531
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - local score fields are part of mapped objective state
10014
10532
  */
10015
10533
  getLocalObjectiveState(activity, objective, isPrimary) {
10016
10534
  if (isPrimary) {
@@ -10019,6 +10537,12 @@ class GlobalObjectiveSynchronizer {
10019
10537
  satisfiedStatus: activity.objectiveSatisfiedStatus,
10020
10538
  measureStatus: activity.objectiveMeasureStatus,
10021
10539
  normalizedMeasure: activity.objectiveNormalizedMeasure,
10540
+ rawScore: objective.rawScore,
10541
+ rawScoreKnown: objective.rawScoreKnown,
10542
+ minScore: objective.minScore,
10543
+ minScoreKnown: objective.minScoreKnown,
10544
+ maxScore: objective.maxScore,
10545
+ maxScoreKnown: objective.maxScoreKnown,
10022
10546
  progressMeasure: activity.progressMeasure,
10023
10547
  progressMeasureStatus: activity.progressMeasureStatus,
10024
10548
  completionStatus: activity.completionStatus,
@@ -10030,6 +10554,12 @@ class GlobalObjectiveSynchronizer {
10030
10554
  satisfiedStatus: objective.satisfiedStatus,
10031
10555
  measureStatus: objective.measureStatus,
10032
10556
  normalizedMeasure: objective.normalizedMeasure,
10557
+ rawScore: objective.rawScore,
10558
+ rawScoreKnown: objective.rawScoreKnown,
10559
+ minScore: objective.minScore,
10560
+ minScoreKnown: objective.minScoreKnown,
10561
+ maxScore: objective.maxScore,
10562
+ maxScoreKnown: objective.maxScoreKnown,
10033
10563
  progressMeasure: objective.progressMeasure,
10034
10564
  progressMeasureStatus: objective.progressMeasureStatus,
10035
10565
  completionStatus: objective.completionStatus,
@@ -10052,7 +10582,7 @@ class GlobalObjectiveSynchronizer {
10052
10582
  (rule) => rule.action === "completed" || rule.action === "incomplete"
10053
10583
  );
10054
10584
  if (globalObjective.satisfiedStatusKnown && globalObjective.satisfiedStatus) {
10055
- if (!hasCompletionRollupRules && (activity.completionStatus === CompletionStatus.UNKNOWN || activity.completionStatus === CompletionStatus.INCOMPLETE)) {
10585
+ if (!hasCompletionRollupRules && !activity.sequencingControls.completionSetByContent && !activity.attemptProgressStatus && (activity.completionStatus === CompletionStatus.UNKNOWN || activity.completionStatus === CompletionStatus.INCOMPLETE)) {
10056
10586
  activity.completionStatus = CompletionStatus.COMPLETED;
10057
10587
  }
10058
10588
  if (activity.successStatus === "unknown") {
@@ -10498,6 +11028,9 @@ class RteDataTransferService {
10498
11028
  * Transfer primary objective data from CMI to activity
10499
11029
  * @param {Activity} activity - The activity to transfer data to
10500
11030
  * @param {CMIDataForTransfer} cmiData - CMI data from runtime
11031
+ *
11032
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - primary objective status and score data
11033
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score write-map source data
10501
11034
  */
10502
11035
  transferPrimaryObjective(activity, cmiData) {
10503
11036
  let hasProgressMeasure = false;
@@ -10544,9 +11077,9 @@ class RteDataTransferService {
10544
11077
  activity.objectiveSatisfiedStatus = successStatus;
10545
11078
  activity.objectiveSatisfiedStatusKnown = true;
10546
11079
  activity.successStatus = validatedSuccessStatus;
10547
- activity.objectiveMeasureStatus = true;
10548
11080
  }
10549
11081
  if (cmiData.score) {
11082
+ activity.primaryObjective?.initializeScoreFromCMI(this.getObjectiveScoreState(cmiData.score));
10550
11083
  const normalized = this.normalizeScore(cmiData.score);
10551
11084
  if (normalized !== null) {
10552
11085
  normalizedScore = normalized;
@@ -10558,7 +11091,7 @@ class RteDataTransferService {
10558
11091
  if (activity.primaryObjective && (hasSuccessStatus || hasNormalizedMeasure)) {
10559
11092
  const finalStatus = hasSuccessStatus ? successStatus : activity.primaryObjective.satisfiedStatus;
10560
11093
  const finalMeasure = hasNormalizedMeasure ? normalizedScore : activity.primaryObjective.normalizedMeasure;
10561
- const measureStatus = hasSuccessStatus || hasNormalizedMeasure;
11094
+ const measureStatus = hasNormalizedMeasure;
10562
11095
  activity.primaryObjective.initializeFromCMI(finalStatus, finalMeasure, measureStatus);
10563
11096
  if (hasSuccessStatus) {
10564
11097
  activity.primaryObjective.satisfiedStatusKnown = true;
@@ -10567,10 +11100,16 @@ class RteDataTransferService {
10567
11100
  }
10568
11101
  }
10569
11102
  /**
10570
- * Transfer non-primary objective data from CMI to activity objectives
10571
- * Only transfers changed values to protect global objectives
11103
+ * Transfer objective-array data from CMI to matching activity objectives.
11104
+ * Only transfers changed values to protect global objectives.
10572
11105
  * @param {Activity} activity - The activity to transfer data to
10573
11106
  * @param {CMIDataForTransfer} cmiData - CMI data from runtime
11107
+ *
11108
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17 - cmi.objectives.n data, including
11109
+ * the primary objective entry, is part of the RTE objective data model.
11110
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 and ADLSEQ objectives extension -
11111
+ * mapped objective status and raw/min/max score data transfer through
11112
+ * objective maps to sequencing state.
10574
11113
  */
10575
11114
  transferNonPrimaryObjectives(activity, cmiData) {
10576
11115
  if (!cmiData.objectives || cmiData.objectives.length === 0) {
@@ -10581,14 +11120,17 @@ class RteDataTransferService {
10581
11120
  continue;
10582
11121
  }
10583
11122
  const activityObjectiveMatch = activity.getObjectiveById(cmiObjective.id);
10584
- if (!activityObjectiveMatch || activityObjectiveMatch.isPrimary) {
11123
+ if (!activityObjectiveMatch) {
10585
11124
  continue;
10586
11125
  }
10587
11126
  const activityObjective = activityObjectiveMatch.objective;
11127
+ const isPrimaryObjective = activityObjectiveMatch.isPrimary;
10588
11128
  let hasSuccessStatus = false;
10589
11129
  let successStatus = false;
10590
11130
  let hasNormalizedMeasure = false;
10591
11131
  let normalizedScore = 0;
11132
+ let hasCompletionStatus = false;
11133
+ let hasProgressMeasure = false;
10592
11134
  const validatedObjSuccessStatus = validateSuccessStatus(cmiObjective.success_status);
10593
11135
  if (validatedObjSuccessStatus && validatedObjSuccessStatus !== SuccessStatus.UNKNOWN) {
10594
11136
  successStatus = validatedObjSuccessStatus === SuccessStatus.PASSED;
@@ -10598,8 +11140,10 @@ class RteDataTransferService {
10598
11140
  const validatedObjCompletionStatus = validateCompletionStatus(cmiObjective.completion_status);
10599
11141
  if (validatedObjCompletionStatus && validatedObjCompletionStatus !== CompletionStatus.UNKNOWN) {
10600
11142
  activityObjective.completionStatus = validatedObjCompletionStatus;
11143
+ hasCompletionStatus = true;
10601
11144
  }
10602
11145
  if (cmiObjective.score) {
11146
+ activityObjective.initializeScoreFromCMI(this.getObjectiveScoreState(cmiObjective.score));
10603
11147
  const normalized = this.normalizeScore(cmiObjective.score);
10604
11148
  if (normalized !== null) {
10605
11149
  normalizedScore = normalized;
@@ -10611,12 +11155,29 @@ class RteDataTransferService {
10611
11155
  const finalMeasure = hasNormalizedMeasure ? normalizedScore : activityObjective.normalizedMeasure;
10612
11156
  const measureStatus = hasNormalizedMeasure;
10613
11157
  activityObjective.initializeFromCMI(finalStatus, finalMeasure, measureStatus);
11158
+ if (hasSuccessStatus) {
11159
+ activityObjective.satisfiedStatusKnown = true;
11160
+ }
10614
11161
  }
10615
11162
  if (cmiObjective.progress_measure && cmiObjective.progress_measure !== "") {
10616
11163
  const progressMeasure = parseFloat(cmiObjective.progress_measure);
10617
11164
  if (!isNaN(progressMeasure)) {
10618
11165
  activityObjective.progressMeasure = progressMeasure;
10619
11166
  activityObjective.progressMeasureStatus = true;
11167
+ hasProgressMeasure = true;
11168
+ }
11169
+ }
11170
+ if (isPrimaryObjective && (hasSuccessStatus || hasNormalizedMeasure || hasCompletionStatus || hasProgressMeasure)) {
11171
+ activityObjective.applyToActivity(activity);
11172
+ if (validatedObjSuccessStatus && validatedObjSuccessStatus !== SuccessStatus.UNKNOWN) {
11173
+ activity.successStatus = validatedObjSuccessStatus;
11174
+ }
11175
+ if (hasCompletionStatus) {
11176
+ activity.attemptProgressStatus = true;
11177
+ }
11178
+ if (hasProgressMeasure) {
11179
+ activity.attemptCompletionAmount = activityObjective.progressMeasure;
11180
+ activity.attemptCompletionAmountStatus = true;
10620
11181
  }
10621
11182
  }
10622
11183
  }
@@ -10645,6 +11206,25 @@ class RteDataTransferService {
10645
11206
  }
10646
11207
  return null;
10647
11208
  }
11209
+ /**
11210
+ * Convert RTE score data into objective score-map state without numeric reformatting.
11211
+ *
11212
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - score values transfer from RTE to sequencing state
11213
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score map fields are independent
11214
+ */
11215
+ getObjectiveScoreState(score) {
11216
+ const scoreState = {};
11217
+ if (score.raw !== void 0) {
11218
+ scoreState.rawScore = score.raw;
11219
+ }
11220
+ if (score.min !== void 0) {
11221
+ scoreState.minScore = score.min;
11222
+ }
11223
+ if (score.max !== void 0) {
11224
+ scoreState.maxScore = score.max;
11225
+ }
11226
+ return scoreState;
11227
+ }
10648
11228
  }
10649
11229
 
10650
11230
  class TerminationHandler {
@@ -11006,13 +11586,7 @@ class TerminationHandler {
11006
11586
  recursionDepth++;
11007
11587
  const exitRules = activity.sequencingRules.exitConditionRules;
11008
11588
  for (const rule of exitRules) {
11009
- let conditionsMet;
11010
- if (rule.conditionCombination === "all") {
11011
- conditionsMet = rule.conditions.every((condition) => condition.evaluate(activity));
11012
- } else {
11013
- conditionsMet = rule.conditions.some((condition) => condition.evaluate(activity));
11014
- }
11015
- if (conditionsMet) {
11589
+ if (rule.evaluate(activity)) {
11016
11590
  if (rule.action === RuleActionType.EXIT) {
11017
11591
  return { action: "EXIT", recursionDepth };
11018
11592
  } else if (rule.action === RuleActionType.EXIT_PARENT) {
@@ -11178,13 +11752,7 @@ class TerminationHandler {
11178
11752
  exitActionRulesSubprocess(activity) {
11179
11753
  const exitRules = activity.sequencingRules.exitConditionRules;
11180
11754
  for (const rule of exitRules) {
11181
- let conditionsMet;
11182
- if (rule.conditionCombination === "all") {
11183
- conditionsMet = rule.conditions.every((condition) => condition.evaluate(activity));
11184
- } else {
11185
- conditionsMet = rule.conditions.some((condition) => condition.evaluate(activity));
11186
- }
11187
- if (conditionsMet) {
11755
+ if (rule.evaluate(activity)) {
11188
11756
  if (rule.action === RuleActionType.EXIT) {
11189
11757
  return "EXIT";
11190
11758
  } else if (rule.action === RuleActionType.EXIT_PARENT) {
@@ -12703,6 +13271,9 @@ class GlobalObjectiveService {
12703
13271
  * Collect Global Objectives
12704
13272
  * Recursively collects global objectives from the activity tree
12705
13273
  * @param {Activity} activity - Activity to collect objectives from
13274
+ *
13275
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objective map contains mapped objective state
13276
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score fields are unknown until written
12706
13277
  */
12707
13278
  collectObjectives(activity) {
12708
13279
  const objectives = activity.getAllObjectives();
@@ -12712,9 +13283,15 @@ class GlobalObjectiveService {
12712
13283
  this.globalObjectiveMap.set(defaultId, {
12713
13284
  id: defaultId,
12714
13285
  satisfiedStatus: activity.objectiveSatisfiedStatus,
12715
- satisfiedStatusKnown: activity.objectiveMeasureStatus,
13286
+ satisfiedStatusKnown: activity.objectiveSatisfiedStatusKnown,
12716
13287
  normalizedMeasure: activity.objectiveNormalizedMeasure,
12717
13288
  normalizedMeasureKnown: activity.objectiveMeasureStatus,
13289
+ rawScore: "",
13290
+ rawScoreKnown: false,
13291
+ minScore: "",
13292
+ minScoreKnown: false,
13293
+ maxScore: "",
13294
+ maxScoreKnown: false,
12718
13295
  progressMeasure: activity.progressMeasure,
12719
13296
  progressMeasureKnown: activity.progressMeasureStatus,
12720
13297
  completionStatus: activity.completionStatus,
@@ -12727,6 +13304,12 @@ class GlobalObjectiveService {
12727
13304
  writeCompletionStatus: true,
12728
13305
  readProgressMeasure: true,
12729
13306
  writeProgressMeasure: true,
13307
+ readRawScore: false,
13308
+ writeRawScore: false,
13309
+ readMinScore: false,
13310
+ writeMinScore: false,
13311
+ readMaxScore: false,
13312
+ writeMaxScore: false,
12730
13313
  satisfiedByMeasure: activity.scaledPassingScore !== null,
12731
13314
  minNormalizedMeasure: activity.scaledPassingScore,
12732
13315
  updateAttemptData: true
@@ -12754,9 +13337,15 @@ class GlobalObjectiveService {
12754
13337
  this.globalObjectiveMap.set(targetId, {
12755
13338
  id: targetId,
12756
13339
  satisfiedStatus: objective.satisfiedStatus,
12757
- satisfiedStatusKnown: objective.measureStatus,
13340
+ satisfiedStatusKnown: objective.satisfiedStatusKnown || objective.progressStatus,
12758
13341
  normalizedMeasure: objective.normalizedMeasure,
12759
13342
  normalizedMeasureKnown: objective.measureStatus,
13343
+ rawScore: "",
13344
+ rawScoreKnown: false,
13345
+ minScore: "",
13346
+ minScoreKnown: false,
13347
+ maxScore: "",
13348
+ maxScoreKnown: false,
12760
13349
  progressMeasure: objective.progressMeasure,
12761
13350
  progressMeasureKnown: objective.progressMeasureStatus,
12762
13351
  completionStatus: objective.completionStatus,
@@ -14739,6 +15328,9 @@ class SequencingService {
14739
15328
  }
14740
15329
  /**
14741
15330
  * Update activity properties from current CMI values
15331
+ *
15332
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - current CMI values update activity objective state
15333
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max scores are available to objective write maps
14742
15334
  */
14743
15335
  updateActivityFromCMI(activity) {
14744
15336
  let hasProgressMeasure = false;
@@ -14770,7 +15362,6 @@ class SequencingService {
14770
15362
  if (this.cmi.success_status !== "unknown") {
14771
15363
  activity.successStatus = this.cmi.success_status;
14772
15364
  activity.objectiveSatisfiedStatus = this.cmi.success_status === "passed";
14773
- activity.objectiveMeasureStatus = true;
14774
15365
  if (activity.primaryObjective) {
14775
15366
  activity.primaryObjective.progressStatus = true;
14776
15367
  }
@@ -14785,6 +15376,17 @@ class SequencingService {
14785
15376
  }
14786
15377
  }
14787
15378
  }
15379
+ if (activity.primaryObjective && this.cmi.score) {
15380
+ if (this.cmi.score.raw !== "") {
15381
+ activity.primaryObjective.rawScore = this.cmi.score.raw;
15382
+ }
15383
+ if (this.cmi.score.min !== "") {
15384
+ activity.primaryObjective.minScore = this.cmi.score.min;
15385
+ }
15386
+ if (this.cmi.score.max !== "") {
15387
+ activity.primaryObjective.maxScore = this.cmi.score.max;
15388
+ }
15389
+ }
14788
15390
  if (activity.primaryObjective) {
14789
15391
  activity.primaryObjective.updateFromActivity(activity);
14790
15392
  }
@@ -15358,15 +15960,22 @@ class SynchronousHttpService {
15358
15960
  const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
15359
15961
  const requestPayload = handledPayload ?? params;
15360
15962
  const { body } = this._prepareRequestBody(requestPayload);
15361
- const beaconSuccess = navigator.sendBeacon(
15362
- url,
15363
- new Blob([body], { type: "text/plain;charset=UTF-8" })
15364
- );
15963
+ const beaconContentType = this.settings.terminationCommitContentType;
15964
+ this._warnIfBeaconContentTypeUnsafe(url, beaconContentType);
15965
+ const beaconSuccess = navigator.sendBeacon(url, new Blob([body], { type: beaconContentType }));
15365
15966
  return {
15366
15967
  result: beaconSuccess ? "true" : "false",
15367
15968
  errorCode: beaconSuccess ? 0 : this.error_codes.GENERAL_COMMIT_FAILURE || 391
15368
15969
  };
15369
15970
  }
15971
+ _warnIfBeaconContentTypeUnsafe(url, contentType) {
15972
+ if (isCrossOriginUrl(url) && !isCorsSafelistedContentType(contentType)) {
15973
+ this.settings.onLogMessage?.(
15974
+ LogLevelEnum.WARN,
15975
+ `sendBeacon to cross-origin URL with non-CORS-safelisted Content-Type "${contentType}" may be silently dropped by the browser (Beacon cannot preflight). Use fetch (useAsynchronousCommits + asyncModeBeaconBehavior:"never") for cross-origin JSON/auth on terminate.`
15976
+ );
15977
+ }
15978
+ }
15370
15979
  /**
15371
15980
  * Performs a synchronous XMLHttpRequest
15372
15981
  * @param {string} url - The URL to send the request to
@@ -16393,8 +17002,8 @@ class BaseAPI {
16393
17002
  * // Throw a "not initialized" error
16394
17003
  * this.throwSCORMError(301, "The API must be initialized before calling GetValue");
16395
17004
  */
16396
- throwSCORMError(CMIElement, errorNumber, message) {
16397
- this._errorHandlingService.throwSCORMError(CMIElement, errorNumber ?? 0, message);
17005
+ throwSCORMError(CMIElement, errorNumber, message, messageLevel) {
17006
+ this._errorHandlingService.throwSCORMError(CMIElement, errorNumber ?? 0, message, messageLevel);
16398
17007
  }
16399
17008
  /**
16400
17009
  * Clears the last SCORM error code when an operation succeeds.
@@ -17297,7 +17906,7 @@ class CMIInteractionsObject extends BaseCMI {
17297
17906
  if (check2004ValidFormat(
17298
17907
  this._cmi_element + ".description",
17299
17908
  description,
17300
- scorm2004_regex.CMILangString250,
17909
+ scorm2004_regex.CMILangString,
17301
17910
  true
17302
17911
  )) {
17303
17912
  this._description = description;
@@ -18201,7 +18810,7 @@ class CMIObjectivesObject extends BaseCMI {
18201
18810
  if (check2004ValidFormat(
18202
18811
  this._cmi_element + ".description",
18203
18812
  description,
18204
- scorm2004_regex.CMILangString250,
18813
+ scorm2004_regex.CMILangString,
18205
18814
  true
18206
18815
  )) {
18207
18816
  this._description = description;
@@ -21044,6 +21653,9 @@ class SequencingConfigurationBuilder {
21044
21653
  if (settings.objectiveSetByContent !== void 0) {
21045
21654
  target.objectiveSetByContent = settings.objectiveSetByContent;
21046
21655
  }
21656
+ if (settings.tracked !== void 0) {
21657
+ target.tracked = settings.tracked;
21658
+ }
21047
21659
  }
21048
21660
  /**
21049
21661
  * Applies the sequencing rules settings to the specified target object.
@@ -21581,6 +22193,12 @@ class ActivityTreeBuilder {
21581
22193
  activitySettings.sequencingControls
21582
22194
  );
21583
22195
  }
22196
+ if (activitySettings.deliveryControls) {
22197
+ this.sequencingConfigBuilder.applySequencingControlsSettings(
22198
+ activity.sequencingControls,
22199
+ activitySettings.deliveryControls
22200
+ );
22201
+ }
21584
22202
  if (activitySettings.sequencingRules) {
21585
22203
  this.sequencingConfigBuilder.applySequencingRulesSettings(
21586
22204
  activity.sequencingRules,
@@ -21670,19 +22288,19 @@ class ActivityTreeBuilder {
21670
22288
  createActivityObjectiveFromSettings(objectiveSettings, isPrimary) {
21671
22289
  const mapInfo = (objectiveSettings.mapInfo || []).map((info) => ({
21672
22290
  targetObjectiveID: info.targetObjectiveID,
21673
- readSatisfiedStatus: info.readSatisfiedStatus ?? false,
21674
- readNormalizedMeasure: info.readNormalizedMeasure ?? false,
22291
+ readSatisfiedStatus: info.readSatisfiedStatus ?? true,
22292
+ readNormalizedMeasure: info.readNormalizedMeasure ?? true,
21675
22293
  writeSatisfiedStatus: info.writeSatisfiedStatus ?? false,
21676
22294
  writeNormalizedMeasure: info.writeNormalizedMeasure ?? false,
21677
- readCompletionStatus: info.readCompletionStatus ?? false,
22295
+ readCompletionStatus: info.readCompletionStatus ?? true,
21678
22296
  writeCompletionStatus: info.writeCompletionStatus ?? false,
21679
- readProgressMeasure: info.readProgressMeasure ?? false,
22297
+ readProgressMeasure: info.readProgressMeasure ?? true,
21680
22298
  writeProgressMeasure: info.writeProgressMeasure ?? false,
21681
- readRawScore: info.readRawScore ?? false,
22299
+ readRawScore: info.readRawScore ?? true,
21682
22300
  writeRawScore: info.writeRawScore ?? false,
21683
- readMinScore: info.readMinScore ?? false,
22301
+ readMinScore: info.readMinScore ?? true,
21684
22302
  writeMinScore: info.writeMinScore ?? false,
21685
- readMaxScore: info.readMaxScore ?? false,
22303
+ readMaxScore: info.readMaxScore ?? true,
21686
22304
  writeMaxScore: info.writeMaxScore ?? false,
21687
22305
  updateAttemptData: info.updateAttemptData ?? false
21688
22306
  }));
@@ -21864,6 +22482,9 @@ class GlobalObjectiveManager {
21864
22482
  *
21865
22483
  * @param {string} objectiveId - The global objective ID
21866
22484
  * @param {CMIObjectivesObject} objective - The CMI objective object with updated values
22485
+ *
22486
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objectives synchronize CMI objective data
22487
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score fields synchronize independently
21867
22488
  */
21868
22489
  updateGlobalObjectiveFromCMI(objectiveId, objective) {
21869
22490
  if (!objectiveId || !this.context.sequencingService) {
@@ -21889,6 +22510,18 @@ class GlobalObjectiveManager {
21889
22510
  updatePayload.normalizedMeasure = normalizedMeasure;
21890
22511
  updatePayload.normalizedMeasureKnown = true;
21891
22512
  }
22513
+ if (objective.score?.raw !== void 0 && objective.score.raw !== "") {
22514
+ updatePayload.rawScore = objective.score.raw;
22515
+ updatePayload.rawScoreKnown = true;
22516
+ }
22517
+ if (objective.score?.min !== void 0 && objective.score.min !== "") {
22518
+ updatePayload.minScore = objective.score.min;
22519
+ updatePayload.minScoreKnown = true;
22520
+ }
22521
+ if (objective.score?.max !== void 0 && objective.score.max !== "") {
22522
+ updatePayload.maxScore = objective.score.max;
22523
+ updatePayload.maxScoreKnown = true;
22524
+ }
21892
22525
  const progressMeasure = this.parseObjectiveNumber(objective.progress_measure);
21893
22526
  if (progressMeasure !== null) {
21894
22527
  updatePayload.progressMeasure = progressMeasure;
@@ -21908,12 +22541,21 @@ class GlobalObjectiveManager {
21908
22541
  *
21909
22542
  * @param {CMIObjectivesObject} objective - The CMI objectives object containing data about a specific learning objective.
21910
22543
  * @return {GlobalObjectiveMapEntry} An object containing mapped properties and their values based on the provided objective.
22544
+ *
22545
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objective map entries capture objective state
22546
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score entries carry per-field known flags
21911
22547
  */
21912
22548
  buildObjectiveMapEntryFromCMI(objective) {
21913
22549
  const entry = {
21914
22550
  id: objective.id,
21915
22551
  satisfiedStatusKnown: false,
21916
22552
  normalizedMeasureKnown: false,
22553
+ rawScore: "",
22554
+ rawScoreKnown: false,
22555
+ minScore: "",
22556
+ minScoreKnown: false,
22557
+ maxScore: "",
22558
+ maxScoreKnown: false,
21917
22559
  progressMeasureKnown: false,
21918
22560
  completionStatusKnown: false,
21919
22561
  readSatisfiedStatus: true,
@@ -21923,7 +22565,13 @@ class GlobalObjectiveManager {
21923
22565
  readCompletionStatus: true,
21924
22566
  writeCompletionStatus: true,
21925
22567
  readProgressMeasure: true,
21926
- writeProgressMeasure: true
22568
+ writeProgressMeasure: true,
22569
+ readRawScore: true,
22570
+ writeRawScore: true,
22571
+ readMinScore: true,
22572
+ writeMinScore: true,
22573
+ readMaxScore: true,
22574
+ writeMaxScore: true
21927
22575
  };
21928
22576
  if (objective.success_status && objective.success_status !== SuccessStatus.UNKNOWN) {
21929
22577
  entry.satisfiedStatus = objective.success_status === SuccessStatus.PASSED;
@@ -21934,6 +22582,18 @@ class GlobalObjectiveManager {
21934
22582
  entry.normalizedMeasure = normalizedMeasure;
21935
22583
  entry.normalizedMeasureKnown = true;
21936
22584
  }
22585
+ if (objective.score?.raw !== void 0 && objective.score.raw !== "") {
22586
+ entry.rawScore = objective.score.raw;
22587
+ entry.rawScoreKnown = true;
22588
+ }
22589
+ if (objective.score?.min !== void 0 && objective.score.min !== "") {
22590
+ entry.minScore = objective.score.min;
22591
+ entry.minScoreKnown = true;
22592
+ }
22593
+ if (objective.score?.max !== void 0 && objective.score.max !== "") {
22594
+ entry.maxScore = objective.score.max;
22595
+ entry.maxScoreKnown = true;
22596
+ }
21937
22597
  const progressMeasure = this.parseObjectiveNumber(objective.progress_measure);
21938
22598
  if (progressMeasure !== null) {
21939
22599
  entry.progressMeasure = progressMeasure;
@@ -21954,6 +22614,9 @@ class GlobalObjectiveManager {
21954
22614
  * @return {CMIObjectivesObject[]} An array of `CMIObjectivesObject` instances built
21955
22615
  * from the provided snapshot map. Returns an empty array
21956
22616
  * if the snapshot is invalid or no valid objectives can be created.
22617
+ *
22618
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objective snapshots restore CMI objective state
22619
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score known fields restore independently
21957
22620
  */
21958
22621
  buildCMIObjectivesFromMap(snapshot) {
21959
22622
  const objectives = [];
@@ -21973,6 +22636,15 @@ class GlobalObjectiveManager {
21973
22636
  if (entry.normalizedMeasureKnown === true && normalizedMeasure !== null) {
21974
22637
  objective.score.scaled = String(normalizedMeasure);
21975
22638
  }
22639
+ if (entry.rawScoreKnown === true) {
22640
+ objective.score.raw = String(entry.rawScore ?? "");
22641
+ }
22642
+ if (entry.minScoreKnown === true) {
22643
+ objective.score.min = String(entry.minScore ?? "");
22644
+ }
22645
+ if (entry.maxScoreKnown === true) {
22646
+ objective.score.max = String(entry.maxScore ?? "");
22647
+ }
21976
22648
  const progressMeasure = this.parseObjectiveNumber(entry.progressMeasure);
21977
22649
  if (entry.progressMeasureKnown === true && progressMeasure !== null) {
21978
22650
  objective.progress_measure = String(progressMeasure);
@@ -22083,6 +22755,9 @@ class GlobalObjectiveManager {
22083
22755
  * @param {CompletionStatus} completionStatus
22084
22756
  * @param {SuccessStatus} successStatus
22085
22757
  * @param {ScoreObject} scoreObject
22758
+ *
22759
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - CMI score data updates the current primary objective
22760
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score data is available for write maps
22086
22761
  */
22087
22762
  syncCmiToSequencingActivity(completionStatus, successStatus, scoreObject) {
22088
22763
  if (!this.context.sequencing) {
@@ -22108,6 +22783,15 @@ class GlobalObjectiveManager {
22108
22783
  primaryObjective.normalizedMeasure = scoreObject.scaled;
22109
22784
  primaryObjective.measureStatus = true;
22110
22785
  }
22786
+ if (scoreObject?.raw !== void 0 && scoreObject.raw !== null) {
22787
+ primaryObjective.rawScore = String(scoreObject.raw);
22788
+ }
22789
+ if (scoreObject?.min !== void 0 && scoreObject.min !== null) {
22790
+ primaryObjective.minScore = String(scoreObject.min);
22791
+ }
22792
+ if (scoreObject?.max !== void 0 && scoreObject.max !== null) {
22793
+ primaryObjective.maxScore = String(scoreObject.max);
22794
+ }
22111
22795
  }
22112
22796
  /**
22113
22797
  * Find or create a global objective by ID
@@ -22660,16 +23344,232 @@ class Scorm2004API extends BaseAPI {
22660
23344
  /**
22661
23345
  * Apply launch-static activity data to CMI while the new SCO is pre-initialize.
22662
23346
  *
22663
- * @spec SCORM 2004 4th Ed. RTE 4.2.9 - cmi.completion_threshold
23347
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5, Table 4.2.5a - cmi.completion_threshold
23348
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives
22664
23349
  */
22665
23350
  applyCurrentActivityLaunchData() {
22666
23351
  const currentActivity = this._sequencing?.getCurrentActivity();
22667
- if (!this.cmi || !currentActivity) {
23352
+ if (!currentActivity) {
23353
+ return;
23354
+ }
23355
+ this.applyActivityLaunchData(currentActivity);
23356
+ }
23357
+ /**
23358
+ * Apply launch-static CMI data when sequencing delivers a new activity before SCO Initialize.
23359
+ *
23360
+ * @spec SCORM 2004 4th Ed. SN DB.2 - Content Delivery Environment Process establishes the delivered activity.
23361
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5, Table 4.2.5a - cmi.completion_threshold is initialized before SCO access.
23362
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives is initialized before SCO access.
23363
+ */
23364
+ applyDeliveredActivityLaunchData(activity) {
23365
+ if (!this.isNotInitialized()) {
23366
+ return;
23367
+ }
23368
+ this.applyActivityLaunchData(activity);
23369
+ }
23370
+ /**
23371
+ * Copy the delivered activity's static launch data into the fresh CMI model.
23372
+ *
23373
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5, Table 4.2.5a - cmi.completion_threshold
23374
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives
23375
+ */
23376
+ applyActivityLaunchData(currentActivity) {
23377
+ if (!this.cmi) {
22668
23378
  return;
22669
23379
  }
22670
23380
  const contentActivityData = this._sequencing.overallSequencingProcess?.getContentActivityData(currentActivity);
22671
23381
  const completionThreshold = contentActivityData?.completionThreshold ?? currentActivity.completionThreshold?.toString() ?? "";
22672
23382
  this.cmi.completion_threshold = completionThreshold;
23383
+ this.seedCurrentActivityObjectives(currentActivity);
23384
+ }
23385
+ /**
23386
+ * Seed CMI objective ids after Initialize when automatic sequencing starts during Initialize.
23387
+ *
23388
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives
23389
+ */
23390
+ applyCurrentActivityObjectiveData() {
23391
+ const currentActivity = this._sequencing?.getCurrentActivity();
23392
+ if (!this.cmi || !currentActivity) {
23393
+ return;
23394
+ }
23395
+ this.seedCurrentActivityObjectives(currentActivity);
23396
+ }
23397
+ /**
23398
+ * Seed CMI objectives from primary and secondary activity objectives.
23399
+ *
23400
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17 - Initialization of Run-Time Objectives from Sequencing Information
23401
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives.n.id and success_status
23402
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - objective read maps seed the RTE view
23403
+ */
23404
+ seedCurrentActivityObjectives(currentActivity) {
23405
+ const activityObjectives = [];
23406
+ if (currentActivity.primaryObjective) {
23407
+ activityObjectives.push(currentActivity.primaryObjective);
23408
+ }
23409
+ activityObjectives.push(...currentActivity.objectives);
23410
+ const seededObjectiveIds = /* @__PURE__ */ new Set();
23411
+ for (const activityObjective of activityObjectives) {
23412
+ const objectiveId = this.getSeedableObjectiveId(activityObjective);
23413
+ if (!objectiveId || seededObjectiveIds.has(objectiveId)) {
23414
+ continue;
23415
+ }
23416
+ seededObjectiveIds.add(objectiveId);
23417
+ const index = this.findOrSeedCMIObjective(objectiveId);
23418
+ if (index === null) {
23419
+ continue;
23420
+ }
23421
+ const cmiObjective = this.cmi.objectives.findObjectiveByIndex(index);
23422
+ if (cmiObjective && this.cmi.objectives.initialized && !cmiObjective.initialized) {
23423
+ cmiObjective.initialize();
23424
+ }
23425
+ const successStatus = this.getActivityObjectiveSuccessStatus(activityObjective);
23426
+ if (successStatus) {
23427
+ this._commonSetCMIValue(
23428
+ "SeedActivityObjective",
23429
+ true,
23430
+ `cmi.objectives.${index}.success_status`,
23431
+ successStatus
23432
+ );
23433
+ }
23434
+ this.seedObjectiveReadMapValues(currentActivity, activityObjective, index);
23435
+ }
23436
+ }
23437
+ /**
23438
+ * Seed CMI objective fields from this objective's read-mapped global objectives.
23439
+ *
23440
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17 - Run-Time Objectives are initialized from sequencing information
23441
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read mapInfo grants access to mapped global objective state
23442
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score read maps seed RTE score fields
23443
+ */
23444
+ seedObjectiveReadMapValues(currentActivity, activityObjective, objectiveIndex) {
23445
+ const globalObjectiveMap = this._sequencing.overallSequencingProcess?.getGlobalObjectiveMap();
23446
+ if (!globalObjectiveMap || activityObjective.mapInfo.length === 0) {
23447
+ return;
23448
+ }
23449
+ for (const mapInfo of activityObjective.mapInfo) {
23450
+ const targetObjectiveId = mapInfo.targetObjectiveID || activityObjective.id;
23451
+ const globalObjective = globalObjectiveMap.get(targetObjectiveId);
23452
+ if (!globalObjective) {
23453
+ continue;
23454
+ }
23455
+ const readState = GlobalObjectiveSynchronizer.getGlobalObjectiveReadState(
23456
+ currentActivity,
23457
+ activityObjective,
23458
+ mapInfo,
23459
+ globalObjective
23460
+ );
23461
+ this.applyObjectiveReadStateToCMI(objectiveIndex, readState);
23462
+ }
23463
+ }
23464
+ /**
23465
+ * Copy mapped global objective state into the seeded CMI objective entry.
23466
+ *
23467
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives launch-time initialization
23468
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read maps populate the RTE view without creating local writes
23469
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score read maps populate CMI objective scores
23470
+ */
23471
+ applyObjectiveReadStateToCMI(objectiveIndex, readState) {
23472
+ if (readState.satisfiedStatus !== void 0) {
23473
+ this._commonSetCMIValue(
23474
+ "SeedActivityObjectiveReadMap",
23475
+ true,
23476
+ `cmi.objectives.${objectiveIndex}.success_status`,
23477
+ readState.satisfiedStatus ? SuccessStatus.PASSED : SuccessStatus.FAILED
23478
+ );
23479
+ }
23480
+ if (readState.normalizedMeasure !== void 0) {
23481
+ this._commonSetCMIValue(
23482
+ "SeedActivityObjectiveReadMap",
23483
+ true,
23484
+ `cmi.objectives.${objectiveIndex}.score.scaled`,
23485
+ String(readState.normalizedMeasure)
23486
+ );
23487
+ }
23488
+ if (readState.completionStatus !== void 0) {
23489
+ this._commonSetCMIValue(
23490
+ "SeedActivityObjectiveReadMap",
23491
+ true,
23492
+ `cmi.objectives.${objectiveIndex}.completion_status`,
23493
+ readState.completionStatus
23494
+ );
23495
+ }
23496
+ if (readState.progressMeasure !== void 0) {
23497
+ this._commonSetCMIValue(
23498
+ "SeedActivityObjectiveReadMap",
23499
+ true,
23500
+ `cmi.objectives.${objectiveIndex}.progress_measure`,
23501
+ String(readState.progressMeasure)
23502
+ );
23503
+ }
23504
+ if (readState.rawScore !== void 0) {
23505
+ this._commonSetCMIValue(
23506
+ "SeedActivityObjectiveReadMap",
23507
+ true,
23508
+ `cmi.objectives.${objectiveIndex}.score.raw`,
23509
+ readState.rawScore
23510
+ );
23511
+ }
23512
+ if (readState.minScore !== void 0) {
23513
+ this._commonSetCMIValue(
23514
+ "SeedActivityObjectiveReadMap",
23515
+ true,
23516
+ `cmi.objectives.${objectiveIndex}.score.min`,
23517
+ readState.minScore
23518
+ );
23519
+ }
23520
+ if (readState.maxScore !== void 0) {
23521
+ this._commonSetCMIValue(
23522
+ "SeedActivityObjectiveReadMap",
23523
+ true,
23524
+ `cmi.objectives.${objectiveIndex}.score.max`,
23525
+ readState.maxScore
23526
+ );
23527
+ }
23528
+ }
23529
+ /**
23530
+ * Return a manifest-defined objective id that can initialize cmi.objectives.n.id.
23531
+ *
23532
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives.n.id
23533
+ */
23534
+ getSeedableObjectiveId(activityObjective) {
23535
+ const objectiveId = activityObjective.id;
23536
+ if (typeof objectiveId !== "string" || objectiveId.trim() === "") {
23537
+ return null;
23538
+ }
23539
+ return objectiveId;
23540
+ }
23541
+ /**
23542
+ * Find an existing CMI objective id or create the next CMI objective array entry.
23543
+ *
23544
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives._count
23545
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives.n.id
23546
+ */
23547
+ findOrSeedCMIObjective(objectiveId) {
23548
+ const existingIndex = this.cmi.objectives.childArray.findIndex((objective) => {
23549
+ return objective.id === objectiveId;
23550
+ });
23551
+ if (existingIndex >= 0) {
23552
+ return existingIndex;
23553
+ }
23554
+ const index = this.cmi.objectives.childArray.length;
23555
+ const result = this._commonSetCMIValue(
23556
+ "SeedActivityObjective",
23557
+ true,
23558
+ `cmi.objectives.${index}.id`,
23559
+ objectiveId
23560
+ );
23561
+ return result === global_constants.SCORM_TRUE ? index : null;
23562
+ }
23563
+ /**
23564
+ * Translate a known activity objective satisfied status to cmi.objectives.n.success_status.
23565
+ *
23566
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives.n.success_status
23567
+ */
23568
+ getActivityObjectiveSuccessStatus(activityObjective) {
23569
+ if (activityObjective.progressStatus || activityObjective.satisfiedStatusKnown) {
23570
+ return activityObjective.satisfiedStatus ? SuccessStatus.PASSED : SuccessStatus.FAILED;
23571
+ }
23572
+ return null;
22673
23573
  }
22674
23574
  /**
22675
23575
  * Getter for _version
@@ -22750,6 +23650,7 @@ class Scorm2004API extends BaseAPI {
22750
23650
  );
22751
23651
  if (result === global_constants.SCORM_TRUE && this._sequencingService) {
22752
23652
  this._sequencingService.initialize();
23653
+ this.applyCurrentActivityObjectiveData();
22753
23654
  }
22754
23655
  if (result === global_constants.SCORM_TRUE) {
22755
23656
  this._globalObjectiveManager.restoreGlobalObjectivesToCMI();
@@ -23004,7 +23905,7 @@ class Scorm2004API extends BaseAPI {
23004
23905
  objective_id = objective ? objective.id : void 0;
23005
23906
  }
23006
23907
  const is_global = objective_id && this.settings.globalObjectiveIds?.includes(objective_id);
23007
- if (is_global) {
23908
+ if (is_global && this.currentActivityAllowsGlobalObjectiveWrites()) {
23008
23909
  const { index: global_index } = this._globalObjectiveManager.findOrCreateGlobalObjective(objective_id);
23009
23910
  const global_element = CMIElement.replace(
23010
23911
  element_base,
@@ -23019,6 +23920,15 @@ class Scorm2004API extends BaseAPI {
23019
23920
  }
23020
23921
  return this._commonSetCMIValue("SetValue", true, CMIElement, value);
23021
23922
  }
23923
+ /**
23924
+ * Return whether the current activity can update shared global objectives.
23925
+ *
23926
+ * @spec SCORM 2004 4th Ed. SN 3.13.1 Tracked - when False, the LMS
23927
+ * "does not initialize, manage or access any tracking status information".
23928
+ */
23929
+ currentActivityAllowsGlobalObjectiveWrites() {
23930
+ return this._sequencing?.getCurrentActivity()?.sequencingControls.tracked !== false;
23931
+ }
23022
23932
  /**
23023
23933
  * Gets or builds a new child element to add to the array
23024
23934
  * @param {string} CMIElement
@@ -23124,7 +24034,8 @@ class Scorm2004API extends BaseAPI {
23124
24034
  this.throwSCORMError(
23125
24035
  CMIElement,
23126
24036
  this._error_codes.VALUE_NOT_INITIALIZED ?? 403,
23127
- `The data model element passed to GetValue (${CMIElement}) has not been initialized.`
24037
+ `The data model element passed to GetValue (${CMIElement}) has not been initialized.`,
24038
+ this.settings.uninitializedGetLogLevel
23128
24039
  );
23129
24040
  }
23130
24041
  /**
@@ -23363,9 +24274,9 @@ class Scorm2004API extends BaseAPI {
23363
24274
  this.loggingService,
23364
24275
  sequencingConfig
23365
24276
  );
23366
- if (settings?.sequencing?.eventListeners) {
23367
- this._sequencingService.setEventListeners(settings.sequencing.eventListeners);
23368
- }
24277
+ this._sequencingService.setEventListeners(
24278
+ this.buildSequencingEventListeners(settings?.sequencing?.eventListeners)
24279
+ );
23369
24280
  this._globalObjectiveManager.updateSequencingService(this._sequencingService);
23370
24281
  this._dataSerializer.updateSequencingService(this._sequencingService);
23371
24282
  if (settings?.sequencingStatePersistence) {
@@ -23388,6 +24299,22 @@ class Scorm2004API extends BaseAPI {
23388
24299
  this._sequencingService = null;
23389
24300
  }
23390
24301
  }
24302
+ /**
24303
+ * Wrap LMS-provided sequencing listeners with API-owned delivery bookkeeping.
24304
+ *
24305
+ * @spec SCORM 2004 4th Ed. SN DB.2 - Content Delivery Environment Process
24306
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5, Table 4.2.5a - cmi.completion_threshold
24307
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives
24308
+ */
24309
+ buildSequencingEventListeners(listeners) {
24310
+ return {
24311
+ ...listeners,
24312
+ onActivityDelivery: (activity) => {
24313
+ this.applyDeliveredActivityLaunchData(activity);
24314
+ listeners?.onActivityDelivery?.(activity);
24315
+ }
24316
+ };
24317
+ }
23391
24318
  /**
23392
24319
  * Get the sequencing service
23393
24320
  * @return {SequencingService | null}
@@ -23398,10 +24325,13 @@ class Scorm2004API extends BaseAPI {
23398
24325
  /**
23399
24326
  * Set sequencing event listeners
23400
24327
  * @param {SequencingEventListeners} listeners
24328
+ *
24329
+ * @spec SCORM 2004 4th Ed. SN DB.2 - Content Delivery Environment Process
24330
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5 / 4.2.17 - launch-static CMI data
23401
24331
  */
23402
24332
  setSequencingEventListeners(listeners) {
23403
24333
  if (this._sequencingService) {
23404
- this._sequencingService.setEventListeners(listeners);
24334
+ this._sequencingService.setEventListeners(this.buildSequencingEventListeners(listeners));
23405
24335
  }
23406
24336
  }
23407
24337
  /**