scorm-again 3.0.4 → 3.1.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 (42) hide show
  1. package/README.md +96 -5
  2. package/dist/cross-frame-api.js.map +1 -1
  3. package/dist/esm/cross-frame-api.js.map +1 -1
  4. package/dist/esm/scorm-again.js +503 -138
  5. package/dist/esm/scorm-again.js.map +1 -1
  6. package/dist/esm/scorm-again.min.js +1 -1
  7. package/dist/esm/scorm-again.min.js.map +1 -1
  8. package/dist/esm/scorm12.js +339 -73
  9. package/dist/esm/scorm12.js.map +1 -1
  10. package/dist/esm/scorm12.min.js +1 -1
  11. package/dist/esm/scorm12.min.js.map +1 -1
  12. package/dist/esm/scorm2004.js +486 -131
  13. package/dist/esm/scorm2004.js.map +1 -1
  14. package/dist/esm/scorm2004.min.js +1 -1
  15. package/dist/esm/scorm2004.min.js.map +1 -1
  16. package/dist/scorm-again.js +443 -119
  17. package/dist/scorm-again.js.map +1 -1
  18. package/dist/scorm-again.min.js +1 -1
  19. package/dist/scorm-again.min.js.map +1 -1
  20. package/dist/scorm12.js +316 -57
  21. package/dist/scorm12.js.map +1 -1
  22. package/dist/scorm12.min.js +1 -1
  23. package/dist/scorm12.min.js.map +1 -1
  24. package/dist/scorm2004.js +431 -112
  25. package/dist/scorm2004.js.map +1 -1
  26. package/dist/scorm2004.min.js +1 -1
  27. package/dist/scorm2004.min.js.map +1 -1
  28. package/dist/types/BaseAPI.d.ts +15 -5
  29. package/dist/types/Scorm12API.d.ts +2 -2
  30. package/dist/types/Scorm2004API.d.ts +3 -2
  31. package/dist/types/cmi/common/score.d.ts +2 -0
  32. package/dist/types/cmi/scorm2004/interaction_delimiters.d.ts +4 -0
  33. package/dist/types/interfaces/services.d.ts +8 -4
  34. package/dist/types/services/AsynchronousHttpService.d.ts +3 -2
  35. package/dist/types/services/EventService.d.ts +2 -2
  36. package/dist/types/services/OfflineStorageService.d.ts +6 -1
  37. package/dist/types/services/SynchronousHttpService.d.ts +2 -2
  38. package/dist/types/services/ValidationService.d.ts +1 -1
  39. package/dist/types/types/api_types.d.ts +22 -2
  40. package/dist/types/utilities/index.d.ts +1 -0
  41. package/dist/types/utilities/url.d.ts +2 -0
  42. package/package.json +46 -25
package/dist/scorm2004.js CHANGED
@@ -198,6 +198,15 @@ this.Scorm2004API = (function () {
198
198
  };
199
199
  }
200
200
 
201
+ const appendQueryParam = (url, name, value) => {
202
+ const fragmentIndex = url.indexOf("#");
203
+ const baseUrl = fragmentIndex === -1 ? url : url.slice(0, fragmentIndex);
204
+ const fragment = fragmentIndex === -1 ? "" : url.slice(fragmentIndex);
205
+ const separator = baseUrl.includes("?") ? baseUrl.endsWith("?") || baseUrl.endsWith("&") ? "" : "&" : "?";
206
+ const queryParam = `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}`;
207
+ return `${baseUrl}${separator}${queryParam}${fragment}`;
208
+ };
209
+
201
210
  var __defProp$15 = Object.defineProperty;
202
211
  var __defNormalProp$15 = (obj, key, value) => key in obj ? __defProp$15(obj, key, {
203
212
  enumerable: true,
@@ -570,7 +579,10 @@ this.Scorm2004API = (function () {
570
579
  INVALID_SET_VALUE: 402,
571
580
  READ_ONLY_ELEMENT: 403,
572
581
  TYPE_MISMATCH: 405,
573
- VALUE_OUT_OF_RANGE: 407};
582
+ // SCORM 1.2 has no out-of-range error code; an out-of-range value is reported
583
+ // as 405 (incorrect data type), per the SCORM 1.2 RTE error table and the ADL
584
+ // 1.2 CTS (DataModelValidator.checkScoreDecimal failure -> CMICategory 405).
585
+ VALUE_OUT_OF_RANGE: 405};
574
586
  const scorm2004_errors = {
575
587
  ...global_errors,
576
588
  INITIALIZATION_FAILED: 102,
@@ -725,6 +737,7 @@ this.Scorm2004API = (function () {
725
737
  xhrWithCredentials: false,
726
738
  fetchMode: "cors",
727
739
  asyncModeBeaconBehavior: "never",
740
+ includeCommitSequence: false,
728
741
  responseHandler: async function (response) {
729
742
  if (typeof response !== "undefined") {
730
743
  let httpResult = null;
@@ -943,6 +956,11 @@ this.Scorm2004API = (function () {
943
956
  progress_range: "0#1"
944
957
  };
945
958
 
959
+ const PERFORMANCE_STEP_NAME = "^$|" + scorm2004_regex.CMIShortIdentifier;
960
+ const PERFORMANCE_CHARACTERSTRING = "(?![\\s\\S]*(?:\\[,\\]|\\[\\.\\]|\\[:\\]))[\\s\\S]{1,250}";
961
+ const PERFORMANCE_NUMERIC_RANGE = "(?:-?\\d+(?:\\.\\d+)?)?\\[:\\](?:-?\\d+(?:\\.\\d+)?)?";
962
+ const CR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_NUMERIC_RANGE + "|" + PERFORMANCE_CHARACTERSTRING + ")$";
963
+ const LR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_CHARACTERSTRING + ")$";
946
964
  const LearnerResponses = {
947
965
  "true-false": {
948
966
  format: "^true$|^false$",
@@ -977,8 +995,8 @@ this.Scorm2004API = (function () {
977
995
  unique: false
978
996
  },
979
997
  performance: {
980
- format: "^$|" + scorm2004_regex.CMIShortIdentifier,
981
- format2: scorm2004_regex.CMIDecimal + "|^$|" + scorm2004_regex.CMIShortIdentifier,
998
+ format: PERFORMANCE_STEP_NAME,
999
+ format2: LR_PERFORMANCE_STEP_ANSWER,
982
1000
  max: 250,
983
1001
  delimiter: "[,]",
984
1002
  delimiter2: "[.]",
@@ -1054,10 +1072,10 @@ this.Scorm2004API = (function () {
1054
1072
  delimiter2: "[.]",
1055
1073
  unique: false,
1056
1074
  duplicate: false,
1057
- // step_name must be a non-empty short identifier
1058
- format: scorm2004_regex.CMIShortIdentifier,
1059
- // step_answer may be short identifier or numeric range (<decimal>[:<decimal>])
1060
- format2: `^(${scorm2004_regex.CMIShortIdentifier})$|^(?:\\d+(?:\\.\\d+)?(?::\\d+(?:\\.\\d+)?)?)$`
1075
+ // step_name: optional short_identifier_type
1076
+ format: PERFORMANCE_STEP_NAME,
1077
+ // step_answer: optional characterstring (spaces allowed) or numeric range
1078
+ format2: CR_PERFORMANCE_STEP_ANSWER
1061
1079
  },
1062
1080
  sequencing: {
1063
1081
  max: 36,
@@ -1133,7 +1151,7 @@ this.Scorm2004API = (function () {
1133
1151
  wrapper() {
1134
1152
  if (!this._cancelled) {
1135
1153
  if (this._API.isInitialized()) {
1136
- (async () => await this._API.commit(this._callback))();
1154
+ (async () => await this._API.commit(this._callback, false, "autocommit"))();
1137
1155
  }
1138
1156
  }
1139
1157
  }
@@ -4606,6 +4624,7 @@ this.Scorm2004API = (function () {
4606
4624
  * @param {ErrorCode} error_codes - The error codes object
4607
4625
  */
4608
4626
  constructor(settings, error_codes) {
4627
+ __publicField$Q(this, "reportsRequestCompletion", true);
4609
4628
  __publicField$Q(this, "settings");
4610
4629
  __publicField$Q(this, "error_codes");
4611
4630
  this.settings = settings;
@@ -4624,13 +4643,17 @@ this.Scorm2004API = (function () {
4624
4643
  * @param {boolean} immediate - Whether to send the request immediately without waiting
4625
4644
  * @param {Function} apiLog - Function to log API messages with appropriate levels
4626
4645
  * @param {Function} processListeners - Function to trigger event listeners for commit events
4646
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
4647
+ * @param {Function} onRequestComplete - Callback invoked after the background request settles
4627
4648
  * @return {ResultObject} - Immediate optimistic success result
4628
4649
  */
4629
4650
  processHttpRequest(url, params) {
4630
4651
  let immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
4631
4652
  let apiLog = arguments.length > 3 ? arguments[3] : undefined;
4632
4653
  let processListeners = arguments.length > 4 ? arguments[4] : undefined;
4633
- this._performAsyncRequest(url, params, immediate, apiLog, processListeners);
4654
+ let metadata = arguments.length > 5 ? arguments[5] : undefined;
4655
+ let onRequestComplete = arguments.length > 6 ? arguments[6] : undefined;
4656
+ this._performAsyncRequest(url, params, immediate, apiLog, processListeners, metadata, onRequestComplete);
4634
4657
  return {
4635
4658
  result: global_constants.SCORM_TRUE,
4636
4659
  errorCode: 0
@@ -4643,11 +4666,14 @@ this.Scorm2004API = (function () {
4643
4666
  * @param {boolean} immediate - Whether this is an immediate request
4644
4667
  * @param apiLog - Function to log API messages
4645
4668
  * @param {Function} processListeners - Function to process event listeners
4669
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
4670
+ * @param {Function} onRequestComplete - Callback invoked after the request settles
4646
4671
  * @private
4647
4672
  */
4648
- async _performAsyncRequest(url, params, immediate, apiLog, processListeners) {
4673
+ async _performAsyncRequest(url, params, immediate, apiLog, processListeners, metadata, onRequestComplete) {
4649
4674
  try {
4650
- const processedParams = this.settings.requestHandler(params);
4675
+ const handledParams = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
4676
+ const processedParams = handledParams;
4651
4677
  let response;
4652
4678
  if (immediate && this.settings.asyncModeBeaconBehavior !== "never") {
4653
4679
  response = await this.performBeacon(url, processedParams);
@@ -4663,7 +4689,9 @@ this.Scorm2004API = (function () {
4663
4689
  } catch (e) {
4664
4690
  const message = e instanceof Error ? e.message : String(e);
4665
4691
  apiLog("processHttpRequest", `Async request failed: ${message}`, LogLevelEnum.ERROR);
4666
- processListeners("CommitError");
4692
+ processListeners("CommitError", void 0, this.error_codes.GENERAL_COMMIT_FAILURE || 391);
4693
+ } finally {
4694
+ onRequestComplete?.();
4667
4695
  }
4668
4696
  }
4669
4697
  /**
@@ -4825,10 +4853,13 @@ this.Scorm2004API = (function () {
4825
4853
  }
4826
4854
  /**
4827
4855
  * Gets the appropriate error code for undefined data model elements.
4828
- * SCORM 2004 uses UNDEFINED_DATA_MODEL, SCORM 1.2 uses GENERAL.
4856
+ * Both SCORM 2004 and SCORM 1.2 use UNDEFINED_DATA_MODEL (401): an
4857
+ * unrecognized element is "Not implemented", not a general exception. SCORM
4858
+ * 1.2 previously returned GENERAL (101) here, which is non-conformant — the
4859
+ * ADL 1.2 CTS and the SCORM 1.2 RTE spec expect 401 for an unknown element.
4829
4860
  */
4830
- getUndefinedDataModelErrorCode(scorm2004) {
4831
- return scorm2004 ? getErrorCode(this.context.errorCodes, "UNDEFINED_DATA_MODEL") : getErrorCode(this.context.errorCodes, "GENERAL");
4861
+ getUndefinedDataModelErrorCode() {
4862
+ return getErrorCode(this.context.errorCodes, "UNDEFINED_DATA_MODEL");
4832
4863
  }
4833
4864
  /**
4834
4865
  * Sets a value on a CMI element path
@@ -4852,7 +4883,7 @@ this.Scorm2004API = (function () {
4852
4883
  let returnValue = global_constants.SCORM_FALSE;
4853
4884
  let foundFirstIndex = false;
4854
4885
  const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
4855
- const invalidErrorCode = this.getUndefinedDataModelErrorCode(scorm2004);
4886
+ const invalidErrorCode = this.getUndefinedDataModelErrorCode();
4856
4887
  for (let idx = 0; idx < structure.length; idx++) {
4857
4888
  const attribute = structure[idx];
4858
4889
  if (idx === structure.length - 1) {
@@ -4892,12 +4923,13 @@ this.Scorm2004API = (function () {
4892
4923
  this.context.throwSCORMError(CMIElement, getErrorCode(this.context.errorCodes, "GENERAL_GET_FAILURE"), "The _version keyword was used incorrectly");
4893
4924
  return "";
4894
4925
  }
4926
+ this.context.setLastErrorCode("0");
4895
4927
  const structure = CMIElement.split(".");
4896
4928
  let refObject = this.context.getDataModel();
4897
4929
  let attribute = null;
4898
4930
  const uninitializedErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) has not been initialized.`;
4899
4931
  const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
4900
- const invalidErrorCode = this.getUndefinedDataModelErrorCode(scorm2004);
4932
+ const invalidErrorCode = this.getUndefinedDataModelErrorCode();
4901
4933
  for (let idx = 0; idx < structure.length; idx++) {
4902
4934
  attribute = structure[idx];
4903
4935
  const validationResult = this.validateGetAttribute(refObject, attribute, CMIElement, scorm2004, invalidErrorCode, invalidErrorMessage, idx === structure.length - 1);
@@ -5082,7 +5114,13 @@ this.Scorm2004API = (function () {
5082
5114
  if (!scorm2004) {
5083
5115
  if (isFinalAttribute) {
5084
5116
  if (typeof attribute === "undefined" || !this.context.checkObjectHasProperty(refObject, attribute)) {
5085
- this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
5117
+ if (attribute === "_children") {
5118
+ this.context.throwSCORMError(CMIElement, getErrorCode(this.context.errorCodes, "CHILDREN_ERROR"));
5119
+ } else if (attribute === "_count") {
5120
+ this.context.throwSCORMError(CMIElement, getErrorCode(this.context.errorCodes, "COUNT_ERROR"));
5121
+ } else {
5122
+ this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
5123
+ }
5086
5124
  return {
5087
5125
  error: true
5088
5126
  };
@@ -5617,8 +5655,9 @@ ${stackTrace}`);
5617
5655
  * @param {string} functionName - The name of the function that triggered the event
5618
5656
  * @param {string} CMIElement - The CMI element that was affected
5619
5657
  * @param {any} value - The value that was set
5658
+ * @param {CommitEventContext} context - Optional context for commit lifecycle events
5620
5659
  */
5621
- processListeners(functionName, CMIElement, value) {
5660
+ processListeners(functionName, CMIElement, value, context) {
5622
5661
  this.apiLog(functionName, value, LogLevelEnum.INFO, CMIElement);
5623
5662
  const listeners = this.listenerMap.get(functionName);
5624
5663
  if (!listeners) return;
@@ -5638,9 +5677,17 @@ ${stackTrace}`);
5638
5677
  if (functionName.startsWith("Sequence")) {
5639
5678
  listener.callback(value);
5640
5679
  } else if (functionName === "CommitError") {
5641
- listener.callback(value);
5680
+ if (context !== void 0) {
5681
+ listener.callback(value, context);
5682
+ } else {
5683
+ listener.callback(value);
5684
+ }
5642
5685
  } else if (functionName === "CommitSuccess") {
5643
- listener.callback();
5686
+ if (context !== void 0) {
5687
+ listener.callback(context);
5688
+ } else {
5689
+ listener.callback();
5690
+ }
5644
5691
  } else {
5645
5692
  listener.callback(CMIElement, value);
5646
5693
  }
@@ -5749,16 +5796,23 @@ ${stackTrace}`);
5749
5796
  * Store commit data offline
5750
5797
  * @param {string} courseId - Identifier for the course
5751
5798
  * @param {CommitObject} commitData - The data to store offline
5799
+ * @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
5752
5800
  * @returns {ResultObject} - Result of the storage operation
5753
5801
  */
5754
- storeOffline(courseId, commitData) {
5802
+ storeOffline(courseId, commitData, metadata) {
5755
5803
  try {
5756
5804
  const queueItem = {
5757
5805
  id: `${courseId}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
5758
5806
  courseId,
5759
5807
  timestamp: Date.now(),
5760
5808
  data: commitData,
5761
- syncAttempts: 0
5809
+ syncAttempts: 0,
5810
+ ...(metadata?.isTerminateCommit !== void 0 ? {
5811
+ isTerminateCommit: metadata.isTerminateCommit
5812
+ } : {}),
5813
+ ...(metadata?.sequence !== void 0 ? {
5814
+ sequence: metadata.sequence
5815
+ } : {})
5762
5816
  };
5763
5817
  const currentQueue = this.getFromStorage(this.syncQueue) || [];
5764
5818
  currentQueue.push(queueItem);
@@ -5817,7 +5871,14 @@ ${stackTrace}`);
5817
5871
  continue;
5818
5872
  }
5819
5873
  try {
5820
- const syncResult = await this.sendDataToLMS(item.data);
5874
+ const syncResult = await this.sendDataToLMS(item.data, {
5875
+ ...(item.isTerminateCommit !== void 0 ? {
5876
+ isTerminateCommit: item.isTerminateCommit
5877
+ } : {}),
5878
+ ...(item.sequence !== void 0 ? {
5879
+ sequence: item.sequence
5880
+ } : {})
5881
+ });
5821
5882
  if (syncResult.result === true || syncResult.result === global_constants.SCORM_TRUE) {
5822
5883
  this.apiLog("OfflineStorageService", `Successfully synced item ${item.id}`, LogLevelEnum.INFO);
5823
5884
  } else {
@@ -5844,17 +5905,27 @@ ${stackTrace}`);
5844
5905
  /**
5845
5906
  * Send data to the LMS when online
5846
5907
  * @param {CommitObject} data - The data to send to the LMS
5908
+ * @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
5847
5909
  * @returns {Promise<ResultObject>} - Result of the sync operation
5848
5910
  */
5849
- async sendDataToLMS(data) {
5850
- if (!this.settings.lmsCommitUrl) {
5911
+ async sendDataToLMS(data, metadata) {
5912
+ const configuredCommitUrl = this.settings.lmsCommitUrl;
5913
+ if (!configuredCommitUrl) {
5851
5914
  return {
5852
5915
  result: global_constants.SCORM_FALSE,
5853
5916
  errorCode: this.error_codes.GENERAL || 101
5854
5917
  };
5855
5918
  }
5856
5919
  try {
5857
- const processedData = this.settings.requestHandler(data);
5920
+ const lmsCommitUrl = String(configuredCommitUrl);
5921
+ const processedData = this.settings.requestHandler(data, {
5922
+ isTerminateCommit: metadata?.isTerminateCommit ?? false,
5923
+ trigger: "offline-replay",
5924
+ ...(metadata?.sequence !== void 0 ? {
5925
+ sequence: metadata.sequence
5926
+ } : {})
5927
+ });
5928
+ const requestUrl = metadata?.isTerminateCommit && this.settings.terminateCommitParam ? appendQueryParam(lmsCommitUrl, this.settings.terminateCommitParam, "true") : lmsCommitUrl;
5858
5929
  const init = {
5859
5930
  method: "POST",
5860
5931
  mode: this.settings.fetchMode,
@@ -5867,7 +5938,7 @@ ${stackTrace}`);
5867
5938
  if (this.settings.xhrWithCredentials) {
5868
5939
  init.credentials = "include";
5869
5940
  }
5870
- const response = await fetch(this.settings.lmsCommitUrl, init);
5941
+ const response = await fetch(requestUrl, init);
5871
5942
  const result = typeof this.settings.responseHandler === "function" ? await this.settings.responseHandler(response) : await response.json();
5872
5943
  if (response.status >= 200 && response.status <= 299 && (result.result === true || result.result === global_constants.SCORM_TRUE)) {
5873
5944
  if (!Object.hasOwnProperty.call(result, "errorCode")) {
@@ -14764,6 +14835,8 @@ ${stackTrace}`);
14764
14835
  * @param {boolean} immediate - Whether this is a termination commit (use sendBeacon)
14765
14836
  * @param {Function} _apiLog - Function to log API messages (unused in synchronous mode - errors returned directly)
14766
14837
  * @param {Function} _processListeners - Function to trigger event listeners (unused in synchronous mode - no async events)
14838
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
14839
+ * @param {Function} _onRequestComplete - Completion callback (unused because requests settle before return)
14767
14840
  * @return {ResultObject} - The result of the request (synchronous)
14768
14841
  *
14769
14842
  * @remarks
@@ -14775,20 +14848,23 @@ ${stackTrace}`);
14775
14848
  */
14776
14849
  processHttpRequest(url, params) {
14777
14850
  let immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
14851
+ let metadata = arguments.length > 5 ? arguments[5] : undefined;
14778
14852
  if (immediate) {
14779
- return this._handleImmediateRequest(url, params);
14853
+ return this._handleImmediateRequest(url, params, metadata);
14780
14854
  }
14781
- return this._performSyncXHR(url, params);
14855
+ return this._performSyncXHR(url, params, metadata);
14782
14856
  }
14783
14857
  /**
14784
14858
  * Handles an immediate request using sendBeacon
14785
14859
  * @param {string} url - The URL to send the request to
14786
14860
  * @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
14861
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
14787
14862
  * @return {ResultObject} - The result based on beacon success
14788
14863
  * @private
14789
14864
  */
14790
- _handleImmediateRequest(url, params) {
14791
- const requestPayload = this.settings.requestHandler(params) ?? params;
14865
+ _handleImmediateRequest(url, params, metadata) {
14866
+ const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
14867
+ const requestPayload = handledPayload ?? params;
14792
14868
  const {
14793
14869
  body
14794
14870
  } = this._prepareRequestBody(requestPayload);
@@ -14804,11 +14880,13 @@ ${stackTrace}`);
14804
14880
  * Performs a synchronous XMLHttpRequest
14805
14881
  * @param {string} url - The URL to send the request to
14806
14882
  * @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
14883
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
14807
14884
  * @return {ResultObject} - The result of the request
14808
14885
  * @private
14809
14886
  */
14810
- _performSyncXHR(url, params) {
14811
- const requestPayload = this.settings.requestHandler(params) ?? params;
14887
+ _performSyncXHR(url, params, metadata) {
14888
+ const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
14889
+ const requestPayload = handledPayload ?? params;
14812
14890
  const {
14813
14891
  body,
14814
14892
  contentType
@@ -14881,9 +14959,16 @@ ${stackTrace}`);
14881
14959
  * @param {number} invalidTypeCode - The error code for invalid type
14882
14960
  * @param {number} invalidRangeCode - The error code for invalid range
14883
14961
  * @param {typeof BaseScormValidationError} errorClass - The error class to use for validation errors
14962
+ * @param {boolean} allowEmptyString - When true, an empty string is accepted (clears the value).
14963
+ * SCORM 1.2 score elements may be blank per the ADL 1.2 CTS
14964
+ * (DataModelValidator.checkScoreDecimal treats blank as valid).
14884
14965
  * @return {boolean} - True if validation passes, throws an error otherwise
14885
14966
  */
14886
14967
  validateScore(CMIElement, value, decimalRegex, scoreRange, invalidTypeCode, invalidRangeCode, errorClass) {
14968
+ let allowEmptyString = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : false;
14969
+ if (allowEmptyString && value === "") {
14970
+ return true;
14971
+ }
14887
14972
  return checkValidFormat(CMIElement, value, decimalRegex, invalidTypeCode, errorClass) && (!scoreRange || checkValidRange(CMIElement, value, scoreRange, invalidRangeCode, errorClass));
14888
14973
  }
14889
14974
  /**
@@ -14979,6 +15064,21 @@ ${stackTrace}`);
14979
15064
  __publicField$o(this, "_offlineStorageService");
14980
15065
  __publicField$o(this, "_cmiValueAccessService");
14981
15066
  __publicField$o(this, "_courseId", "");
15067
+ __publicField$o(this, "_pendingCommitCount", 0);
15068
+ /**
15069
+ * Monotonic sequence for commits captured by this API instance. It is
15070
+ * intentionally not reset by reset().
15071
+ */
15072
+ __publicField$o(this, "_commitSequence", 0);
15073
+ __publicField$o(this, "_commitSettleWaiters", []);
15074
+ /**
15075
+ * Canonical paths of every CMI element that has been explicitly assigned a
15076
+ * value via SetValue / loadFromJSON (both funnel through _commonSetCMIValue).
15077
+ * Used by standards that must tell "implemented but never set" apart from a
15078
+ * legitimately empty value when answering GetValue (SCORM 2004 error 403).
15079
+ * Cleared on reset so a fresh SCO attempt starts with nothing "set".
15080
+ */
15081
+ __publicField$o(this, "_setCMIElements", /* @__PURE__ */new Set());
14982
15082
  __publicField$o(this, "startingData");
14983
15083
  __publicField$o(this, "currentState");
14984
15084
  if (new.target === BaseAPI) {
@@ -15125,6 +15225,7 @@ ${stackTrace}`);
15125
15225
  this.lastErrorCode = "0";
15126
15226
  this._eventService.reset();
15127
15227
  this.startingData = {};
15228
+ this._setCMIElements.clear();
15128
15229
  if (this._offlineStorageService) {
15129
15230
  this._offlineStorageService.updateSettings(this.settings);
15130
15231
  if (settings?.courseId) {
@@ -15208,6 +15309,54 @@ ${stackTrace}`);
15208
15309
  this._loggingService?.setLogHandler(settings.onLogMessage);
15209
15310
  }
15210
15311
  }
15312
+ /**
15313
+ * Gets the number of captured commit requests that have not yet settled.
15314
+ *
15315
+ * @return {number} The number of in-flight commits
15316
+ */
15317
+ get pendingCommitCount() {
15318
+ return this._pendingCommitCount;
15319
+ }
15320
+ /**
15321
+ * Resolves when all currently in-flight commits have settled. A timeout is
15322
+ * best-effort: the promise resolves when it elapses even if commits remain,
15323
+ * and callers can inspect pendingCommitCount afterward to detect that case.
15324
+ *
15325
+ * @param {Object} [options] - Settle options
15326
+ * @param {number} [options.timeoutMs] - Maximum time to wait in milliseconds
15327
+ * @return {Promise<void>} A promise that resolves after the drain or timeout
15328
+ */
15329
+ whenCommitsSettled(options) {
15330
+ if (this._pendingCommitCount === 0) {
15331
+ return Promise.resolve();
15332
+ }
15333
+ return new Promise(resolve => {
15334
+ const waiter = {
15335
+ resolve
15336
+ };
15337
+ if (options?.timeoutMs !== void 0) {
15338
+ waiter.timeoutId = setTimeout(() => {
15339
+ const waiterIndex = this._commitSettleWaiters.indexOf(waiter);
15340
+ if (waiterIndex === -1) {
15341
+ return;
15342
+ }
15343
+ this._commitSettleWaiters.splice(waiterIndex, 1);
15344
+ resolve();
15345
+ }, options.timeoutMs);
15346
+ }
15347
+ this._commitSettleWaiters.push(waiter);
15348
+ });
15349
+ }
15350
+ /** Resolve and clear every waiter after the pending count reaches zero. */
15351
+ _flushCommitSettleWaiters() {
15352
+ const waiters = this._commitSettleWaiters.splice(0);
15353
+ for (const waiter of waiters) {
15354
+ if (waiter.timeoutId !== void 0) {
15355
+ clearTimeout(waiter.timeoutId);
15356
+ }
15357
+ waiter.resolve();
15358
+ }
15359
+ }
15211
15360
  /**
15212
15361
  * Terminates the current run of the API
15213
15362
  * @param {string} callbackName
@@ -15228,7 +15377,7 @@ ${stackTrace}`);
15228
15377
  } else {
15229
15378
  stateCheckPassed = true;
15230
15379
  this.processListeners("BeforeTerminate");
15231
- const result = this.storeData(true);
15380
+ const result = this.storeData(true, "terminate");
15232
15381
  if ((result.errorCode ?? 0) > 0) {
15233
15382
  if (result.errorMessage) {
15234
15383
  this.apiLog("terminate", `Terminate failed with error: ${result.errorMessage}`, LogLevelEnum.ERROR);
@@ -15268,6 +15417,9 @@ ${stackTrace}`);
15268
15417
  } catch (e) {
15269
15418
  returnValue = this.handleValueAccessException(CMIElement, e, returnValue);
15270
15419
  }
15420
+ if (this.lastErrorCode === "0") {
15421
+ this.checkUninitializedGet(CMIElement, returnValue);
15422
+ }
15271
15423
  this.processListeners(callbackName, CMIElement);
15272
15424
  }
15273
15425
  this.apiLog(callbackName, ": returned: " + returnValue, LogLevelEnum.INFO, CMIElement);
@@ -15277,6 +15429,10 @@ ${stackTrace}`);
15277
15429
  if (this.lastErrorCode === "0") {
15278
15430
  this.clearSCORMError(returnValue);
15279
15431
  }
15432
+ const rawReturn = returnValue;
15433
+ if (typeof rawReturn === "number" || typeof rawReturn === "boolean") {
15434
+ return String(rawReturn);
15435
+ }
15280
15436
  return returnValue;
15281
15437
  }
15282
15438
  /**
@@ -15320,10 +15476,12 @@ ${stackTrace}`);
15320
15476
  * Orders LMS to store all content parameters
15321
15477
  * @param {string} callbackName
15322
15478
  * @param {boolean} checkTerminated
15479
+ * @param {CommitTrigger} trigger - What initiated the commit
15323
15480
  * @return {string}
15324
15481
  */
15325
15482
  commit(callbackName) {
15326
15483
  let checkTerminated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
15484
+ let trigger = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "manual";
15327
15485
  this.clearScheduledCommit();
15328
15486
  let returnValue = global_constants.SCORM_TRUE;
15329
15487
  if (this.isNotInitialized()) {
@@ -15335,20 +15493,21 @@ ${stackTrace}`);
15335
15493
  this.throwSCORMError("api", errorCode);
15336
15494
  if (errorCode === 143) returnValue = global_constants.SCORM_FALSE;
15337
15495
  } else {
15338
- const result = this.storeData(false);
15339
- if ((result.errorCode ?? 0) > 0) {
15496
+ const result = this.storeData(false, trigger);
15497
+ const errorCode = result.errorCode ?? 0;
15498
+ if (errorCode > 0) {
15340
15499
  if (result.errorMessage) {
15341
15500
  this.apiLog("commit", `Commit failed with error: ${result.errorMessage}`, LogLevelEnum.ERROR);
15342
15501
  }
15343
15502
  if (result.errorDetails) {
15344
15503
  this.apiLog("commit", `Error details: ${JSON.stringify(result.errorDetails)}`, LogLevelEnum.DEBUG);
15345
15504
  }
15346
- this.throwSCORMError("api", result.errorCode);
15505
+ this.throwSCORMError("api", errorCode);
15347
15506
  }
15348
15507
  const resultValue = result?.result ?? global_constants.SCORM_FALSE;
15349
15508
  returnValue = typeof resultValue === "boolean" ? String(resultValue) : resultValue;
15350
15509
  this.apiLog(callbackName, " Result: " + returnValue, LogLevelEnum.DEBUG, "HttpRequest");
15351
- if (checkTerminated) this.lastErrorCode = "0";
15510
+ if (checkTerminated && errorCode === 0) this.lastErrorCode = "0";
15352
15511
  this.processListeners(callbackName);
15353
15512
  if (this.settings.enableOfflineSupport && this._offlineStorageService && this._offlineStorageService.isDeviceOnline() && this._courseId) {
15354
15513
  this._offlineStorageService.hasPendingOfflineData(this._courseId).then(hasPendingData => {
@@ -15555,7 +15714,11 @@ ${stackTrace}`);
15555
15714
  * @return {string}
15556
15715
  */
15557
15716
  _commonSetCMIValue(methodName, scorm2004, CMIElement, value) {
15558
- return this._cmiValueAccessService.setCMIValue(methodName, scorm2004, CMIElement, value);
15717
+ const result = this._cmiValueAccessService.setCMIValue(methodName, scorm2004, CMIElement, value);
15718
+ if (result === global_constants.SCORM_TRUE) {
15719
+ this._setCMIElements.add(CMIElement);
15720
+ }
15721
+ return result;
15559
15722
  }
15560
15723
  /**
15561
15724
  * Gets a value from the CMI Object.
@@ -15569,6 +15732,17 @@ ${stackTrace}`);
15569
15732
  _commonGetCMIValue(methodName, scorm2004, CMIElement) {
15570
15733
  return this._cmiValueAccessService.getCMIValue(methodName, scorm2004, CMIElement);
15571
15734
  }
15735
+ /**
15736
+ * Hook invoked by getValue after a successful resolution. Standards that must
15737
+ * distinguish "implemented but never set, no default value" from a
15738
+ * legitimately empty value override this to raise VALUE_NOT_INITIALIZED.
15739
+ * Default is a no-op, so SCORM 1.2 / AICC keep returning "" with code 0.
15740
+ *
15741
+ * @param {string} _CMIElement - the element that was read
15742
+ * @param {any} _returnValue - the value getCMIValue resolved
15743
+ * @protected
15744
+ */
15745
+ checkUninitializedGet(_CMIElement, _returnValue) {}
15572
15746
  /**
15573
15747
  * Returns true if the API's current state is STATE_INITIALIZED
15574
15748
  *
@@ -15651,9 +15825,14 @@ ${stackTrace}`);
15651
15825
  * @param {string} functionName - The name of the function/event that occurred
15652
15826
  * @param {string} CMIElement - Optional CMI element involved in the event
15653
15827
  * @param {any} value - Optional value associated with the event
15828
+ * @param {CommitEventContext} context - Optional context for commit lifecycle events
15654
15829
  */
15655
- processListeners(functionName, CMIElement, value) {
15656
- this._eventService.processListeners(functionName, CMIElement, value);
15830
+ processListeners(functionName, CMIElement, value, context) {
15831
+ if (context !== void 0) {
15832
+ this._eventService.processListeners(functionName, CMIElement, value, context);
15833
+ } else {
15834
+ this._eventService.processListeners(functionName, CMIElement, value);
15835
+ }
15657
15836
  }
15658
15837
  /**
15659
15838
  * Throws a SCORM error with the specified error number and optional message.
@@ -15775,23 +15954,97 @@ ${stackTrace}`);
15775
15954
  * @param {string} url - The URL to send the request to
15776
15955
  * @param {CommitObject | StringKeyMap | Array<any>} params - The parameters to send
15777
15956
  * @param {boolean} immediate - Whether to send the request immediately without waiting
15957
+ * @param {CommitTrigger} [trigger] - What initiated the commit
15778
15958
  * @returns {ResultObject} - The result of the request
15779
15959
  */
15780
15960
  processHttpRequest(url, params) {
15781
15961
  let immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
15782
- if (this.settings.enableOfflineSupport && this._offlineStorageService && !this._offlineStorageService.isDeviceOnline() && this._courseId) {
15783
- this.apiLog("processHttpRequest", "Device is offline, storing data locally", LogLevelEnum.INFO);
15784
- if (params && typeof params === "object" && "cmi" in params) {
15785
- return this._offlineStorageService.storeOffline(this._courseId, params);
15786
- } else {
15787
- this.apiLog("processHttpRequest", "Invalid commit data format for offline storage", LogLevelEnum.ERROR);
15788
- return {
15789
- result: global_constants.SCORM_FALSE,
15790
- errorCode: this._error_codes.GENERAL ?? 101
15791
- };
15962
+ let trigger = arguments.length > 3 ? arguments[3] : undefined;
15963
+ const sequence = ++this._commitSequence;
15964
+ this._pendingCommitCount += 1;
15965
+ let settled = false;
15966
+ let completionDeferred = false;
15967
+ const settle = () => {
15968
+ if (settled) {
15969
+ return;
15970
+ }
15971
+ settled = true;
15972
+ this._pendingCommitCount -= 1;
15973
+ if (this._pendingCommitCount === 0) {
15974
+ this._flushCommitSettleWaiters();
15975
+ }
15976
+ };
15977
+ try {
15978
+ const resolvedTrigger = trigger ?? (immediate ? "terminate" : "manual");
15979
+ let finalParams = params;
15980
+ if (immediate && this.settings.terminateCommitPayloadField) {
15981
+ const field = this.settings.terminateCommitPayloadField;
15982
+ if (Array.isArray(finalParams)) {
15983
+ finalParams = [...finalParams, `${encodeURIComponent(field)}=true`];
15984
+ } else if (finalParams && typeof finalParams === "object") {
15985
+ finalParams = {
15986
+ ...finalParams,
15987
+ [field]: true
15988
+ };
15989
+ }
15990
+ }
15991
+ if (this.settings.includeCommitSequence === true) {
15992
+ if (Array.isArray(finalParams)) {
15993
+ finalParams = [...finalParams, `commitSequence=${sequence}`];
15994
+ } else if (finalParams && typeof finalParams === "object") {
15995
+ finalParams = {
15996
+ ...finalParams,
15997
+ commitSequence: sequence
15998
+ };
15999
+ }
16000
+ }
16001
+ const finalUrl = immediate && this.settings.terminateCommitParam ? appendQueryParam(url, this.settings.terminateCommitParam, "true") : url;
16002
+ const metadata = {
16003
+ isTerminateCommit: immediate,
16004
+ trigger: resolvedTrigger,
16005
+ sequence
16006
+ };
16007
+ const context = {
16008
+ url: finalUrl,
16009
+ trigger: resolvedTrigger,
16010
+ isTerminateCommit: immediate,
16011
+ sequence
16012
+ };
16013
+ if (this.settings.enableOfflineSupport && this._offlineStorageService && !this._offlineStorageService.isDeviceOnline() && this._courseId) {
16014
+ this.apiLog("processHttpRequest", "Device is offline, storing data locally", LogLevelEnum.INFO);
16015
+ if (finalParams && typeof finalParams === "object" && "cmi" in finalParams) {
16016
+ return this._offlineStorageService.storeOffline(this._courseId, finalParams, {
16017
+ isTerminateCommit: immediate,
16018
+ sequence
16019
+ });
16020
+ } else {
16021
+ this.apiLog("processHttpRequest", "Invalid commit data format for offline storage", LogLevelEnum.ERROR);
16022
+ return {
16023
+ result: global_constants.SCORM_FALSE,
16024
+ errorCode: this._error_codes.GENERAL ?? 101
16025
+ };
16026
+ }
16027
+ }
16028
+ const apiLog = (functionName, message, level, element) => this.apiLog(functionName, message, level, element);
16029
+ const processListeners = (functionName, CMIElement, value) => {
16030
+ if (functionName === "CommitSuccess" || functionName === "CommitError") {
16031
+ if (functionName === "CommitError" && typeof value === "number") {
16032
+ context.errorCode = value;
16033
+ }
16034
+ settle();
16035
+ this.processListeners(functionName, CMIElement, value, context);
16036
+ } else {
16037
+ this.processListeners(functionName, CMIElement, value);
16038
+ }
16039
+ };
16040
+ const result = this._httpService.processHttpRequest(finalUrl, finalParams, immediate, apiLog, processListeners, metadata, settle);
16041
+ completionDeferred = this._httpService.reportsRequestCompletion === true;
16042
+ return result;
16043
+ } finally {
16044
+ if (!completionDeferred) {
16045
+ settle();
15792
16046
  }
15793
16047
  }
15794
- return this._httpService.processHttpRequest(url, params, immediate, (functionName, message, level, element) => this.apiLog(functionName, message, level, element), (functionName, CMIElement, value) => this.processListeners(functionName, CMIElement, value));
15795
16048
  }
15796
16049
  /**
15797
16050
  * Schedules a commit operation to occur after a specified delay.
@@ -16065,6 +16318,49 @@ ${stackTrace}`);
16065
16318
  }
16066
16319
  }
16067
16320
 
16321
+ function stripBrackets(delim) {
16322
+ return delim.replace(/[[\]]/g, "");
16323
+ }
16324
+ function escapeRegex(s) {
16325
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16326
+ }
16327
+ function splitDelimited(value, bracketed) {
16328
+ if (!bracketed) {
16329
+ return [value];
16330
+ }
16331
+ if (value.includes(bracketed)) {
16332
+ return value.split(bracketed);
16333
+ }
16334
+ const bare = stripBrackets(bracketed);
16335
+ if (!bare) {
16336
+ return [value];
16337
+ }
16338
+ const splitRe = new RegExp(`(?<!\\\\)${escapeRegex(bare)}`, "g");
16339
+ const unescapeRe = new RegExp(`\\\\${escapeRegex(bare)}`, "g");
16340
+ return value.split(splitRe).map(part => part.replace(unescapeRe, bare));
16341
+ }
16342
+ function splitFirstDelimited(value, bracketed) {
16343
+ if (!bracketed) {
16344
+ return [value];
16345
+ }
16346
+ if (value.includes(bracketed)) {
16347
+ const idx = value.indexOf(bracketed);
16348
+ return [value.slice(0, idx), value.slice(idx + bracketed.length)];
16349
+ }
16350
+ const bare = stripBrackets(bracketed);
16351
+ if (!bare) {
16352
+ return [value];
16353
+ }
16354
+ const splitRe = new RegExp(`(?<!\\\\)${escapeRegex(bare)}`);
16355
+ const unescapeRe = new RegExp(`\\\\${escapeRegex(bare)}`, "g");
16356
+ const parts = value.split(splitRe);
16357
+ const first = (parts[0] ?? "").replace(unescapeRe, bare);
16358
+ if (parts.length === 1) {
16359
+ return [first];
16360
+ }
16361
+ return [first, parts.slice(1).join(bare).replace(unescapeRe, bare)];
16362
+ }
16363
+
16068
16364
  var __defProp$m = Object.defineProperty;
16069
16365
  var __defNormalProp$m = (obj, key, value) => key in obj ? __defProp$m(obj, key, {
16070
16366
  enumerable: true,
@@ -16260,8 +16556,7 @@ ${stackTrace}`);
16260
16556
  const response_type = LearnerResponses[this.type];
16261
16557
  if (response_type) {
16262
16558
  if (response_type?.delimiter) {
16263
- const delimiter = response_type.delimiter === "[,]" ? "," : response_type.delimiter;
16264
- nodes = learner_response.split(delimiter);
16559
+ nodes = splitDelimited(learner_response, response_type.delimiter);
16265
16560
  } else {
16266
16561
  nodes[0] = learner_response;
16267
16562
  }
@@ -16269,10 +16564,10 @@ ${stackTrace}`);
16269
16564
  const formatRegex = new RegExp(response_type.format);
16270
16565
  for (let i = 0; i < nodes.length; i++) {
16271
16566
  if (response_type?.delimiter2) {
16272
- const delimiter2 = response_type.delimiter2 === "[.]" ? "." : response_type.delimiter2;
16273
- const values = nodes[i]?.split(delimiter2);
16567
+ const node = nodes[i] ?? "";
16568
+ const values = this.type === "performance" ? splitFirstDelimited(node, response_type.delimiter2) : splitDelimited(node, response_type.delimiter2);
16274
16569
  if (values?.length === 2) {
16275
- if (this.type === "performance" && (values[0] === "" || values[1] === "")) {
16570
+ if (this.type === "performance" && values[0] === "" && values[1] === "") {
16276
16571
  throw new Scorm2004ValidationError(this._cmi_element + ".learner_response", scorm2004_errors.TYPE_MISMATCH);
16277
16572
  }
16278
16573
  if (!values[0]?.match(formatRegex)) {
@@ -16453,37 +16748,19 @@ ${stackTrace}`);
16453
16748
  return result;
16454
16749
  }
16455
16750
  }
16456
- function stripBrackets(delim) {
16457
- return delim.replace(/[[\]]/g, "");
16458
- }
16459
- function escapeRegex(s) {
16460
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16461
- }
16462
- function splitUnescaped(text, delim) {
16463
- const reDelim = escapeRegex(delim);
16464
- const splitRe = new RegExp(`(?<!\\\\)${reDelim}`, "g");
16465
- const unescapeRe = new RegExp(`\\\\${reDelim}`, "g");
16466
- return text.split(splitRe).map(part => part.replace(unescapeRe, delim));
16467
- }
16468
- function splitFirstUnescaped(text, delim) {
16469
- const reDelim = escapeRegex(delim);
16470
- const splitRe = new RegExp(`(?<!\\\\)${reDelim}`);
16471
- const unescapeRe = new RegExp(`\\\\${reDelim}`, "g");
16472
- const parts = text.split(splitRe);
16473
- const firstPart = parts[0] ?? "";
16474
- if (parts.length === 1) {
16475
- return [firstPart.replace(unescapeRe, delim)];
16751
+ const RESPONSE_PREFIX_RE = /^\{(?:lang|case_matters|order_matters)=[^}]+\}/;
16752
+ function stripResponsePrefixes(node) {
16753
+ let result = node;
16754
+ while (RESPONSE_PREFIX_RE.test(result)) {
16755
+ result = result.replace(RESPONSE_PREFIX_RE, "");
16476
16756
  }
16477
- const part1 = firstPart.replace(unescapeRe, delim);
16478
- const part2 = parts.slice(1).join(delim).replace(unescapeRe, delim);
16479
- return [part1, part2];
16757
+ return result;
16480
16758
  }
16481
16759
  function validatePattern(type, pattern, responseDef) {
16482
16760
  if (pattern.trim() !== pattern) {
16483
16761
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
16484
16762
  }
16485
- const subDelim1 = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : null;
16486
- const rawNodes = subDelim1 ? splitUnescaped(pattern, subDelim1) : [pattern];
16763
+ const rawNodes = responseDef.delimiter ? splitDelimited(pattern, responseDef.delimiter) : [pattern];
16487
16764
  for (const raw of rawNodes) {
16488
16765
  if (raw.trim() !== raw) {
16489
16766
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
@@ -16492,17 +16769,11 @@ ${stackTrace}`);
16492
16769
  if (type === "fill-in" && pattern === "") {
16493
16770
  return;
16494
16771
  }
16495
- const delim1 = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : null;
16496
- let nodes;
16497
- if (delim1) {
16498
- nodes = splitUnescaped(pattern, delim1);
16499
- } else {
16500
- nodes = [pattern];
16501
- }
16772
+ const nodes = responseDef.delimiter ? splitDelimited(pattern, responseDef.delimiter) : [pattern];
16502
16773
  if (!responseDef.delimiter && pattern.includes(",")) {
16503
16774
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
16504
16775
  }
16505
- if (responseDef.unique || responseDef.duplicate === false) {
16776
+ if (type !== "numeric" && (responseDef.unique || responseDef.duplicate === false)) {
16506
16777
  const seen = new Set(nodes);
16507
16778
  if (seen.size !== nodes.length) {
16508
16779
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
@@ -16522,8 +16793,7 @@ ${stackTrace}`);
16522
16793
  if (!delimBracketed) {
16523
16794
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
16524
16795
  }
16525
- const delim = stripBrackets(delimBracketed);
16526
- const parts = value.split(new RegExp(`(?<!\\\\)${escapeRegex(delim)}`, "g")).map(n => n.replace(new RegExp(`\\\\${escapeRegex(delim)}`, "g"), delim));
16796
+ const parts = splitDelimited(value, delimBracketed);
16527
16797
  if (parts.length !== 2 || parts[0] === "" || parts[1] === "") {
16528
16798
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
16529
16799
  }
@@ -16535,12 +16805,14 @@ ${stackTrace}`);
16535
16805
  switch (type) {
16536
16806
  case "numeric":
16537
16807
  {
16538
- const numDelim = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : ":";
16539
- const nums = node.split(numDelim);
16540
- if (nums.length < 1 || nums.length > 2) {
16541
- throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
16808
+ if (node === "") {
16809
+ const bracketedRange = nodes.length >= 2 && !!responseDef.delimiter && pattern.includes(responseDef.delimiter);
16810
+ if (!bracketedRange) {
16811
+ throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
16812
+ }
16813
+ break;
16542
16814
  }
16543
- nums.forEach(checkSingle);
16815
+ checkSingle(node);
16544
16816
  break;
16545
16817
  }
16546
16818
  case "performance":
@@ -16549,13 +16821,13 @@ ${stackTrace}`);
16549
16821
  if (!delimBracketed) {
16550
16822
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
16551
16823
  }
16552
- const delim = stripBrackets(delimBracketed);
16553
- const parts = splitFirstUnescaped(node, delim);
16824
+ const record = stripResponsePrefixes(node);
16825
+ const parts = splitFirstDelimited(record, delimBracketed);
16554
16826
  if (parts.length !== 2) {
16555
16827
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
16556
16828
  }
16557
16829
  const [part1, part2] = parts;
16558
- if (part1 === "" || part2 === "" || part1 === part2) {
16830
+ if (part1 === "" && part2 === "") {
16559
16831
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
16560
16832
  }
16561
16833
  if (part1 === void 0 || !fmt1.test(part1)) {
@@ -16665,6 +16937,11 @@ ${stackTrace}`);
16665
16937
  __publicField$l(this, "__invalid_range_code");
16666
16938
  __publicField$l(this, "__decimal_regex");
16667
16939
  __publicField$l(this, "__error_class");
16940
+ /**
16941
+ * When true, an empty string is a valid value (clears the element). SCORM 1.2
16942
+ * score elements may be blank per the ADL 1.2 CTS; SCORM 2004 leaves this off.
16943
+ */
16944
+ __publicField$l(this, "__allow_empty_string");
16668
16945
  __publicField$l(this, "_raw", "");
16669
16946
  __publicField$l(this, "_min", "");
16670
16947
  __publicField$l(this, "_max");
@@ -16676,6 +16953,7 @@ ${stackTrace}`);
16676
16953
  this.__invalid_range_code = params.invalidRangeCode || scorm12_errors.VALUE_OUT_OF_RANGE;
16677
16954
  this.__decimal_regex = params.decimalRegex || scorm12_regex.CMIDecimal;
16678
16955
  this.__error_class = params.errorClass;
16956
+ this.__allow_empty_string = params.allowEmptyString ?? false;
16679
16957
  }
16680
16958
  /**
16681
16959
  * Called when the API has been reset
@@ -16715,7 +16993,7 @@ ${stackTrace}`);
16715
16993
  * @param {string} raw
16716
16994
  */
16717
16995
  set raw(raw) {
16718
- if (validationService.validateScore(this._cmi_element + ".raw", raw, this.__decimal_regex, this.__score_range, this.__invalid_type_code, this.__invalid_range_code, this.__error_class)) {
16996
+ if (validationService.validateScore(this._cmi_element + ".raw", raw, this.__decimal_regex, this.__score_range, this.__invalid_type_code, this.__invalid_range_code, this.__error_class, this.__allow_empty_string)) {
16719
16997
  this._raw = raw;
16720
16998
  }
16721
16999
  }
@@ -16731,7 +17009,7 @@ ${stackTrace}`);
16731
17009
  * @param {string} min
16732
17010
  */
16733
17011
  set min(min) {
16734
- if (validationService.validateScore(this._cmi_element + ".min", min, this.__decimal_regex, this.__score_range, this.__invalid_type_code, this.__invalid_range_code, this.__error_class)) {
17012
+ if (validationService.validateScore(this._cmi_element + ".min", min, this.__decimal_regex, this.__score_range, this.__invalid_type_code, this.__invalid_range_code, this.__error_class, this.__allow_empty_string)) {
16735
17013
  this._min = min;
16736
17014
  }
16737
17015
  }
@@ -16747,7 +17025,7 @@ ${stackTrace}`);
16747
17025
  * @param {string} max
16748
17026
  */
16749
17027
  set max(max) {
16750
- if (validationService.validateScore(this._cmi_element + ".max", max, this.__decimal_regex, this.__score_range, this.__invalid_type_code, this.__invalid_range_code, this.__error_class)) {
17028
+ if (validationService.validateScore(this._cmi_element + ".max", max, this.__decimal_regex, this.__score_range, this.__invalid_type_code, this.__invalid_range_code, this.__error_class, this.__allow_empty_string)) {
16751
17029
  this._max = max;
16752
17030
  }
16753
17031
  }
@@ -19510,7 +19788,7 @@ ${stackTrace}`);
19510
19788
  checkValidResponseType(CMIElement, response_type, value, interaction_type) {
19511
19789
  let nodes = [];
19512
19790
  if (response_type?.delimiter) {
19513
- nodes = String(value).split(response_type.delimiter);
19791
+ nodes = splitDelimited(String(value), response_type.delimiter);
19514
19792
  } else {
19515
19793
  nodes[0] = value;
19516
19794
  }
@@ -19608,26 +19886,33 @@ ${stackTrace}`);
19608
19886
  nodes[i] = this.removeCorrectResponsePrefixes(CMIElement, nodes[i]);
19609
19887
  }
19610
19888
  if (response?.delimiter2) {
19611
- const values = nodes[i].split(response.delimiter2);
19889
+ const values = interaction_type === "performance" ? splitFirstDelimited(nodes[i], response.delimiter2) : splitDelimited(nodes[i], response.delimiter2);
19612
19890
  if (values.length === 2) {
19613
- const matches = values[0].match(formatRegex);
19614
- if (!matches) {
19891
+ if (interaction_type === "performance" && values[0] === "" && values[1] === "") {
19615
19892
  this.context.throwSCORMError(CMIElement, scorm2004_errors.TYPE_MISMATCH, `${interaction_type}: ${value}`);
19616
19893
  } else {
19617
- if (!response.format2 || !values[1].match(new RegExp(response.format2))) {
19894
+ const matches = values[0]?.match(formatRegex);
19895
+ if (!matches) {
19618
19896
  this.context.throwSCORMError(CMIElement, scorm2004_errors.TYPE_MISMATCH, `${interaction_type}: ${value}`);
19897
+ } else {
19898
+ if (!response.format2 || !values[1]?.match(new RegExp(response.format2))) {
19899
+ this.context.throwSCORMError(CMIElement, scorm2004_errors.TYPE_MISMATCH, `${interaction_type}: ${value}`);
19900
+ }
19619
19901
  }
19620
19902
  }
19621
19903
  } else {
19622
19904
  this.context.throwSCORMError(CMIElement, scorm2004_errors.TYPE_MISMATCH, `${interaction_type}: ${value}`);
19623
19905
  }
19624
19906
  } else {
19907
+ if (interaction_type === "numeric" && nodes.length > 1 && nodes[i] === "" && !!response.delimiter && String(value).includes(response.delimiter)) {
19908
+ continue;
19909
+ }
19625
19910
  const matches = nodes[i].match(formatRegex);
19626
19911
  if (!matches && value !== "" || !matches && interaction_type === "true-false") {
19627
19912
  this.context.throwSCORMError(CMIElement, scorm2004_errors.TYPE_MISMATCH, `${interaction_type}: ${value}`);
19628
19913
  } else {
19629
19914
  if (interaction_type === "numeric" && nodes.length > 1) {
19630
- if (Number(nodes[0]) > Number(nodes[1])) {
19915
+ if (nodes[0] !== "" && nodes[1] !== "" && Number(nodes[0]) > Number(nodes[1])) {
19631
19916
  this.context.throwSCORMError(CMIElement, scorm2004_errors.TYPE_MISMATCH, `${interaction_type}: ${value}`);
19632
19917
  }
19633
19918
  } else {
@@ -21306,6 +21591,10 @@ ${stackTrace}`);
21306
21591
  value
21307
21592
  }) : obj[key] = value;
21308
21593
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
21594
+ const NO_DEFAULT_2004_ELEMENTS = /* @__PURE__ */new Set(["cmi.suspend_data", "cmi.location", "cmi.scaled_passing_score", "cmi.max_time_allowed", "cmi.completion_threshold", "cmi.progress_measure", "cmi.score.scaled", "cmi.score.raw", "cmi.score.min", "cmi.score.max", "cmi.objectives.N.score.scaled", "cmi.objectives.N.score.raw", "cmi.objectives.N.score.min", "cmi.objectives.N.score.max", "cmi.objectives.N.progress_measure", "cmi.objectives.N.description", "cmi.interactions.N.weighting", "cmi.interactions.N.type", "cmi.interactions.N.timestamp", "cmi.interactions.N.result", "cmi.interactions.N.latency", "cmi.interactions.N.learner_response", "cmi.interactions.N.description", "cmi.comments_from_learner.N.timestamp"]);
21595
+ function normalizeCMIIndices(CMIElement) {
21596
+ return CMIElement.replace(/\.\d+(?=\.|$)/g, ".N");
21597
+ }
21309
21598
  class Scorm2004API extends BaseAPI {
21310
21599
  /**
21311
21600
  * Constructor for SCORM 2004 API
@@ -21805,6 +22094,35 @@ ${stackTrace}`);
21805
22094
  getCMIValue(CMIElement) {
21806
22095
  return this._commonGetCMIValue("GetValue", true, CMIElement);
21807
22096
  }
22097
+ /**
22098
+ * Raises 403 (VALUE_NOT_INITIALIZED) when an implemented, no-default element is
22099
+ * read before it has ever been set, per IEEE 1484.11.2 / SCORM 2004 RTE.
22100
+ *
22101
+ * Invoked by BaseAPI.getValue after an otherwise-successful resolution, so it
22102
+ * only sees values the data model already returned cleanly. The decision is
22103
+ * deliberately gated to this public boundary (never the getters) to keep
22104
+ * serialization/commit/rollup — which read the getters directly — unaffected.
22105
+ *
22106
+ * A 403 is raised only when all hold:
22107
+ * - the resolved value is "" — any non-empty value is by definition set,
22108
+ * including values written by internal sequencing/activity-tree paths that
22109
+ * bypass _commonSetCMIValue (those only ever write non-empty values);
22110
+ * - the element was never set — _setCMIElements records every successful
22111
+ * SetValue, loadFromJSON, and global-objective restore (all route through
22112
+ * _commonSetCMIValue), so an explicit SetValue(x, "") still reads back as
22113
+ * "" with code 0; and
22114
+ * - the element has no spec-defined default ({@link NO_DEFAULT_2004_ELEMENTS}).
22115
+ *
22116
+ * @param {string} CMIElement
22117
+ * @param {*} returnValue - the value getCMIValue resolved
22118
+ * @protected
22119
+ */
22120
+ checkUninitializedGet(CMIElement, returnValue) {
22121
+ if (returnValue !== "") return;
22122
+ if (this._setCMIElements.has(CMIElement)) return;
22123
+ if (!NO_DEFAULT_2004_ELEMENTS.has(normalizeCMIIndices(CMIElement))) return;
22124
+ this.throwSCORMError(CMIElement, this._error_codes.VALUE_NOT_INITIALIZED ?? 403, `The data model element passed to GetValue (${CMIElement}) has not been initialized.`);
22125
+ }
21808
22126
  /**
21809
22127
  * Returns the message that corresponds to errorNumber.
21810
22128
  * @param {(string|number)} errorNumber
@@ -21853,9 +22171,10 @@ ${stackTrace}`);
21853
22171
  /**
21854
22172
  * Attempts to store the data to the LMS
21855
22173
  * @param {boolean} terminateCommit
22174
+ * @param {CommitTrigger} [trigger] - What initiated the commit
21856
22175
  * @return {ResultObject}
21857
22176
  */
21858
- storeData(terminateCommit) {
22177
+ storeData(terminateCommit, trigger) {
21859
22178
  if (terminateCommit) {
21860
22179
  if (this.cmi.mode === "normal") {
21861
22180
  if (this.cmi.credit === "credit") {
@@ -21896,7 +22215,7 @@ ${stackTrace}`);
21896
22215
  }
21897
22216
  this._globalObjectiveManager.syncCmiToSequencingActivity(completionStatusEnum, successStatusEnum, scoreObject);
21898
22217
  if (typeof this.settings.lmsCommitUrl === "string") {
21899
- const result = this.processHttpRequest(this.settings.lmsCommitUrl, commitObject, terminateCommit);
22218
+ const result = this.processHttpRequest(this.settings.lmsCommitUrl, commitObject, terminateCommit, trigger);
21900
22219
  if (navRequest && result.navRequest !== void 0 && result.navRequest !== "" && typeof result.navRequest === "string") {
21901
22220
  const parsed = parseNavigationRequest(result.navRequest);
21902
22221
  if (!parsed.valid) {