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
@@ -252,6 +252,15 @@
252
252
  };
253
253
  }
254
254
 
255
+ const appendQueryParam = (url, name, value) => {
256
+ const fragmentIndex = url.indexOf("#");
257
+ const baseUrl = fragmentIndex === -1 ? url : url.slice(0, fragmentIndex);
258
+ const fragment = fragmentIndex === -1 ? "" : url.slice(fragmentIndex);
259
+ const separator = baseUrl.includes("?") ? baseUrl.endsWith("?") || baseUrl.endsWith("&") ? "" : "&" : "?";
260
+ const queryParam = `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}`;
261
+ return `${baseUrl}${separator}${queryParam}${fragment}`;
262
+ };
263
+
255
264
  var __defProp$1d = Object.defineProperty;
256
265
  var __defNormalProp$1d = (obj, key, value) => key in obj ? __defProp$1d(obj, key, {
257
266
  enumerable: true,
@@ -642,7 +651,10 @@
642
651
  READ_ONLY_ELEMENT: 403,
643
652
  WRITE_ONLY_ELEMENT: 404,
644
653
  TYPE_MISMATCH: 405,
645
- VALUE_OUT_OF_RANGE: 407,
654
+ // SCORM 1.2 has no out-of-range error code; an out-of-range value is reported
655
+ // as 405 (incorrect data type), per the SCORM 1.2 RTE error table and the ADL
656
+ // 1.2 CTS (DataModelValidator.checkScoreDecimal failure -> CMICategory 405).
657
+ VALUE_OUT_OF_RANGE: 405,
646
658
  DEPENDENCY_NOT_ESTABLISHED: 408
647
659
  };
648
660
  const scorm2004_errors = {
@@ -799,6 +811,7 @@
799
811
  xhrWithCredentials: false,
800
812
  fetchMode: "cors",
801
813
  asyncModeBeaconBehavior: "never",
814
+ includeCommitSequence: false,
802
815
  responseHandler: async function (response) {
803
816
  if (typeof response !== "undefined") {
804
817
  let httpResult = null;
@@ -1159,6 +1172,11 @@
1159
1172
  progress_range: "0#1"
1160
1173
  };
1161
1174
 
1175
+ const PERFORMANCE_STEP_NAME = "^$|" + scorm2004_regex.CMIShortIdentifier;
1176
+ const PERFORMANCE_CHARACTERSTRING = "(?![\\s\\S]*(?:\\[,\\]|\\[\\.\\]|\\[:\\]))[\\s\\S]{1,250}";
1177
+ const PERFORMANCE_NUMERIC_RANGE = "(?:-?\\d+(?:\\.\\d+)?)?\\[:\\](?:-?\\d+(?:\\.\\d+)?)?";
1178
+ const CR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_NUMERIC_RANGE + "|" + PERFORMANCE_CHARACTERSTRING + ")$";
1179
+ const LR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_CHARACTERSTRING + ")$";
1162
1180
  const LearnerResponses = {
1163
1181
  "true-false": {
1164
1182
  format: "^true$|^false$",
@@ -1193,8 +1211,8 @@
1193
1211
  unique: false
1194
1212
  },
1195
1213
  performance: {
1196
- format: "^$|" + scorm2004_regex.CMIShortIdentifier,
1197
- format2: scorm2004_regex.CMIDecimal + "|^$|" + scorm2004_regex.CMIShortIdentifier,
1214
+ format: PERFORMANCE_STEP_NAME,
1215
+ format2: LR_PERFORMANCE_STEP_ANSWER,
1198
1216
  max: 250,
1199
1217
  delimiter: "[,]",
1200
1218
  delimiter2: "[.]",
@@ -1270,10 +1288,10 @@
1270
1288
  delimiter2: "[.]",
1271
1289
  unique: false,
1272
1290
  duplicate: false,
1273
- // step_name must be a non-empty short identifier
1274
- format: scorm2004_regex.CMIShortIdentifier,
1275
- // step_answer may be short identifier or numeric range (<decimal>[:<decimal>])
1276
- format2: `^(${scorm2004_regex.CMIShortIdentifier})$|^(?:\\d+(?:\\.\\d+)?(?::\\d+(?:\\.\\d+)?)?)$`
1291
+ // step_name: optional short_identifier_type
1292
+ format: PERFORMANCE_STEP_NAME,
1293
+ // step_answer: optional characterstring (spaces allowed) or numeric range
1294
+ format2: CR_PERFORMANCE_STEP_ANSWER
1277
1295
  },
1278
1296
  sequencing: {
1279
1297
  max: 36,
@@ -1349,7 +1367,7 @@
1349
1367
  wrapper() {
1350
1368
  if (!this._cancelled) {
1351
1369
  if (this._API.isInitialized()) {
1352
- (async () => await this._API.commit(this._callback))();
1370
+ (async () => await this._API.commit(this._callback, false, "autocommit"))();
1353
1371
  }
1354
1372
  }
1355
1373
  }
@@ -4822,6 +4840,7 @@
4822
4840
  * @param {ErrorCode} error_codes - The error codes object
4823
4841
  */
4824
4842
  constructor(settings, error_codes) {
4843
+ __publicField$Y(this, "reportsRequestCompletion", true);
4825
4844
  __publicField$Y(this, "settings");
4826
4845
  __publicField$Y(this, "error_codes");
4827
4846
  this.settings = settings;
@@ -4840,13 +4859,17 @@
4840
4859
  * @param {boolean} immediate - Whether to send the request immediately without waiting
4841
4860
  * @param {Function} apiLog - Function to log API messages with appropriate levels
4842
4861
  * @param {Function} processListeners - Function to trigger event listeners for commit events
4862
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
4863
+ * @param {Function} onRequestComplete - Callback invoked after the background request settles
4843
4864
  * @return {ResultObject} - Immediate optimistic success result
4844
4865
  */
4845
4866
  processHttpRequest(url, params) {
4846
4867
  let immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
4847
4868
  let apiLog = arguments.length > 3 ? arguments[3] : undefined;
4848
4869
  let processListeners = arguments.length > 4 ? arguments[4] : undefined;
4849
- this._performAsyncRequest(url, params, immediate, apiLog, processListeners);
4870
+ let metadata = arguments.length > 5 ? arguments[5] : undefined;
4871
+ let onRequestComplete = arguments.length > 6 ? arguments[6] : undefined;
4872
+ this._performAsyncRequest(url, params, immediate, apiLog, processListeners, metadata, onRequestComplete);
4850
4873
  return {
4851
4874
  result: global_constants.SCORM_TRUE,
4852
4875
  errorCode: 0
@@ -4859,11 +4882,14 @@
4859
4882
  * @param {boolean} immediate - Whether this is an immediate request
4860
4883
  * @param apiLog - Function to log API messages
4861
4884
  * @param {Function} processListeners - Function to process event listeners
4885
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
4886
+ * @param {Function} onRequestComplete - Callback invoked after the request settles
4862
4887
  * @private
4863
4888
  */
4864
- async _performAsyncRequest(url, params, immediate, apiLog, processListeners) {
4889
+ async _performAsyncRequest(url, params, immediate, apiLog, processListeners, metadata, onRequestComplete) {
4865
4890
  try {
4866
- const processedParams = this.settings.requestHandler(params);
4891
+ const handledParams = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
4892
+ const processedParams = handledParams;
4867
4893
  let response;
4868
4894
  if (immediate && this.settings.asyncModeBeaconBehavior !== "never") {
4869
4895
  response = await this.performBeacon(url, processedParams);
@@ -4879,7 +4905,9 @@
4879
4905
  } catch (e) {
4880
4906
  const message = e instanceof Error ? e.message : String(e);
4881
4907
  apiLog("processHttpRequest", `Async request failed: ${message}`, LogLevelEnum.ERROR);
4882
- processListeners("CommitError");
4908
+ processListeners("CommitError", void 0, this.error_codes.GENERAL_COMMIT_FAILURE || 391);
4909
+ } finally {
4910
+ onRequestComplete?.();
4883
4911
  }
4884
4912
  }
4885
4913
  /**
@@ -5041,10 +5069,13 @@
5041
5069
  }
5042
5070
  /**
5043
5071
  * Gets the appropriate error code for undefined data model elements.
5044
- * SCORM 2004 uses UNDEFINED_DATA_MODEL, SCORM 1.2 uses GENERAL.
5072
+ * Both SCORM 2004 and SCORM 1.2 use UNDEFINED_DATA_MODEL (401): an
5073
+ * unrecognized element is "Not implemented", not a general exception. SCORM
5074
+ * 1.2 previously returned GENERAL (101) here, which is non-conformant — the
5075
+ * ADL 1.2 CTS and the SCORM 1.2 RTE spec expect 401 for an unknown element.
5045
5076
  */
5046
- getUndefinedDataModelErrorCode(scorm2004) {
5047
- return scorm2004 ? getErrorCode(this.context.errorCodes, "UNDEFINED_DATA_MODEL") : getErrorCode(this.context.errorCodes, "GENERAL");
5077
+ getUndefinedDataModelErrorCode() {
5078
+ return getErrorCode(this.context.errorCodes, "UNDEFINED_DATA_MODEL");
5048
5079
  }
5049
5080
  /**
5050
5081
  * Sets a value on a CMI element path
@@ -5068,7 +5099,7 @@
5068
5099
  let returnValue = global_constants.SCORM_FALSE;
5069
5100
  let foundFirstIndex = false;
5070
5101
  const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
5071
- const invalidErrorCode = this.getUndefinedDataModelErrorCode(scorm2004);
5102
+ const invalidErrorCode = this.getUndefinedDataModelErrorCode();
5072
5103
  for (let idx = 0; idx < structure.length; idx++) {
5073
5104
  const attribute = structure[idx];
5074
5105
  if (idx === structure.length - 1) {
@@ -5108,12 +5139,13 @@
5108
5139
  this.context.throwSCORMError(CMIElement, getErrorCode(this.context.errorCodes, "GENERAL_GET_FAILURE"), "The _version keyword was used incorrectly");
5109
5140
  return "";
5110
5141
  }
5142
+ this.context.setLastErrorCode("0");
5111
5143
  const structure = CMIElement.split(".");
5112
5144
  let refObject = this.context.getDataModel();
5113
5145
  let attribute = null;
5114
5146
  const uninitializedErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) has not been initialized.`;
5115
5147
  const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
5116
- const invalidErrorCode = this.getUndefinedDataModelErrorCode(scorm2004);
5148
+ const invalidErrorCode = this.getUndefinedDataModelErrorCode();
5117
5149
  for (let idx = 0; idx < structure.length; idx++) {
5118
5150
  attribute = structure[idx];
5119
5151
  const validationResult = this.validateGetAttribute(refObject, attribute, CMIElement, scorm2004, invalidErrorCode, invalidErrorMessage, idx === structure.length - 1);
@@ -5298,7 +5330,13 @@
5298
5330
  if (!scorm2004) {
5299
5331
  if (isFinalAttribute) {
5300
5332
  if (typeof attribute === "undefined" || !this.context.checkObjectHasProperty(refObject, attribute)) {
5301
- this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
5333
+ if (attribute === "_children") {
5334
+ this.context.throwSCORMError(CMIElement, getErrorCode(this.context.errorCodes, "CHILDREN_ERROR"));
5335
+ } else if (attribute === "_count") {
5336
+ this.context.throwSCORMError(CMIElement, getErrorCode(this.context.errorCodes, "COUNT_ERROR"));
5337
+ } else {
5338
+ this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
5339
+ }
5302
5340
  return {
5303
5341
  error: true
5304
5342
  };
@@ -5833,8 +5871,9 @@ ${stackTrace}`);
5833
5871
  * @param {string} functionName - The name of the function that triggered the event
5834
5872
  * @param {string} CMIElement - The CMI element that was affected
5835
5873
  * @param {any} value - The value that was set
5874
+ * @param {CommitEventContext} context - Optional context for commit lifecycle events
5836
5875
  */
5837
- processListeners(functionName, CMIElement, value) {
5876
+ processListeners(functionName, CMIElement, value, context) {
5838
5877
  this.apiLog(functionName, value, LogLevelEnum.INFO, CMIElement);
5839
5878
  const listeners = this.listenerMap.get(functionName);
5840
5879
  if (!listeners) return;
@@ -5854,9 +5893,17 @@ ${stackTrace}`);
5854
5893
  if (functionName.startsWith("Sequence")) {
5855
5894
  listener.callback(value);
5856
5895
  } else if (functionName === "CommitError") {
5857
- listener.callback(value);
5896
+ if (context !== void 0) {
5897
+ listener.callback(value, context);
5898
+ } else {
5899
+ listener.callback(value);
5900
+ }
5858
5901
  } else if (functionName === "CommitSuccess") {
5859
- listener.callback();
5902
+ if (context !== void 0) {
5903
+ listener.callback(context);
5904
+ } else {
5905
+ listener.callback();
5906
+ }
5860
5907
  } else {
5861
5908
  listener.callback(CMIElement, value);
5862
5909
  }
@@ -5965,16 +6012,23 @@ ${stackTrace}`);
5965
6012
  * Store commit data offline
5966
6013
  * @param {string} courseId - Identifier for the course
5967
6014
  * @param {CommitObject} commitData - The data to store offline
6015
+ * @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
5968
6016
  * @returns {ResultObject} - Result of the storage operation
5969
6017
  */
5970
- storeOffline(courseId, commitData) {
6018
+ storeOffline(courseId, commitData, metadata) {
5971
6019
  try {
5972
6020
  const queueItem = {
5973
6021
  id: `${courseId}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
5974
6022
  courseId,
5975
6023
  timestamp: Date.now(),
5976
6024
  data: commitData,
5977
- syncAttempts: 0
6025
+ syncAttempts: 0,
6026
+ ...(metadata?.isTerminateCommit !== void 0 ? {
6027
+ isTerminateCommit: metadata.isTerminateCommit
6028
+ } : {}),
6029
+ ...(metadata?.sequence !== void 0 ? {
6030
+ sequence: metadata.sequence
6031
+ } : {})
5978
6032
  };
5979
6033
  const currentQueue = this.getFromStorage(this.syncQueue) || [];
5980
6034
  currentQueue.push(queueItem);
@@ -6033,7 +6087,14 @@ ${stackTrace}`);
6033
6087
  continue;
6034
6088
  }
6035
6089
  try {
6036
- const syncResult = await this.sendDataToLMS(item.data);
6090
+ const syncResult = await this.sendDataToLMS(item.data, {
6091
+ ...(item.isTerminateCommit !== void 0 ? {
6092
+ isTerminateCommit: item.isTerminateCommit
6093
+ } : {}),
6094
+ ...(item.sequence !== void 0 ? {
6095
+ sequence: item.sequence
6096
+ } : {})
6097
+ });
6037
6098
  if (syncResult.result === true || syncResult.result === global_constants.SCORM_TRUE) {
6038
6099
  this.apiLog("OfflineStorageService", `Successfully synced item ${item.id}`, LogLevelEnum.INFO);
6039
6100
  } else {
@@ -6060,17 +6121,27 @@ ${stackTrace}`);
6060
6121
  /**
6061
6122
  * Send data to the LMS when online
6062
6123
  * @param {CommitObject} data - The data to send to the LMS
6124
+ * @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
6063
6125
  * @returns {Promise<ResultObject>} - Result of the sync operation
6064
6126
  */
6065
- async sendDataToLMS(data) {
6066
- if (!this.settings.lmsCommitUrl) {
6127
+ async sendDataToLMS(data, metadata) {
6128
+ const configuredCommitUrl = this.settings.lmsCommitUrl;
6129
+ if (!configuredCommitUrl) {
6067
6130
  return {
6068
6131
  result: global_constants.SCORM_FALSE,
6069
6132
  errorCode: this.error_codes.GENERAL || 101
6070
6133
  };
6071
6134
  }
6072
6135
  try {
6073
- const processedData = this.settings.requestHandler(data);
6136
+ const lmsCommitUrl = String(configuredCommitUrl);
6137
+ const processedData = this.settings.requestHandler(data, {
6138
+ isTerminateCommit: metadata?.isTerminateCommit ?? false,
6139
+ trigger: "offline-replay",
6140
+ ...(metadata?.sequence !== void 0 ? {
6141
+ sequence: metadata.sequence
6142
+ } : {})
6143
+ });
6144
+ const requestUrl = metadata?.isTerminateCommit && this.settings.terminateCommitParam ? appendQueryParam(lmsCommitUrl, this.settings.terminateCommitParam, "true") : lmsCommitUrl;
6074
6145
  const init = {
6075
6146
  method: "POST",
6076
6147
  mode: this.settings.fetchMode,
@@ -6083,7 +6154,7 @@ ${stackTrace}`);
6083
6154
  if (this.settings.xhrWithCredentials) {
6084
6155
  init.credentials = "include";
6085
6156
  }
6086
- const response = await fetch(this.settings.lmsCommitUrl, init);
6157
+ const response = await fetch(requestUrl, init);
6087
6158
  const result = typeof this.settings.responseHandler === "function" ? await this.settings.responseHandler(response) : await response.json();
6088
6159
  if (response.status >= 200 && response.status <= 299 && (result.result === true || result.result === global_constants.SCORM_TRUE)) {
6089
6160
  if (!Object.hasOwnProperty.call(result, "errorCode")) {
@@ -14980,6 +15051,8 @@ ${stackTrace}`);
14980
15051
  * @param {boolean} immediate - Whether this is a termination commit (use sendBeacon)
14981
15052
  * @param {Function} _apiLog - Function to log API messages (unused in synchronous mode - errors returned directly)
14982
15053
  * @param {Function} _processListeners - Function to trigger event listeners (unused in synchronous mode - no async events)
15054
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
15055
+ * @param {Function} _onRequestComplete - Completion callback (unused because requests settle before return)
14983
15056
  * @return {ResultObject} - The result of the request (synchronous)
14984
15057
  *
14985
15058
  * @remarks
@@ -14991,20 +15064,23 @@ ${stackTrace}`);
14991
15064
  */
14992
15065
  processHttpRequest(url, params) {
14993
15066
  let immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
15067
+ let metadata = arguments.length > 5 ? arguments[5] : undefined;
14994
15068
  if (immediate) {
14995
- return this._handleImmediateRequest(url, params);
15069
+ return this._handleImmediateRequest(url, params, metadata);
14996
15070
  }
14997
- return this._performSyncXHR(url, params);
15071
+ return this._performSyncXHR(url, params, metadata);
14998
15072
  }
14999
15073
  /**
15000
15074
  * Handles an immediate request using sendBeacon
15001
15075
  * @param {string} url - The URL to send the request to
15002
15076
  * @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
15077
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
15003
15078
  * @return {ResultObject} - The result based on beacon success
15004
15079
  * @private
15005
15080
  */
15006
- _handleImmediateRequest(url, params) {
15007
- const requestPayload = this.settings.requestHandler(params) ?? params;
15081
+ _handleImmediateRequest(url, params, metadata) {
15082
+ const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
15083
+ const requestPayload = handledPayload ?? params;
15008
15084
  const {
15009
15085
  body
15010
15086
  } = this._prepareRequestBody(requestPayload);
@@ -15020,11 +15096,13 @@ ${stackTrace}`);
15020
15096
  * Performs a synchronous XMLHttpRequest
15021
15097
  * @param {string} url - The URL to send the request to
15022
15098
  * @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
15099
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
15023
15100
  * @return {ResultObject} - The result of the request
15024
15101
  * @private
15025
15102
  */
15026
- _performSyncXHR(url, params) {
15027
- const requestPayload = this.settings.requestHandler(params) ?? params;
15103
+ _performSyncXHR(url, params, metadata) {
15104
+ const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
15105
+ const requestPayload = handledPayload ?? params;
15028
15106
  const {
15029
15107
  body,
15030
15108
  contentType
@@ -15097,9 +15175,16 @@ ${stackTrace}`);
15097
15175
  * @param {number} invalidTypeCode - The error code for invalid type
15098
15176
  * @param {number} invalidRangeCode - The error code for invalid range
15099
15177
  * @param {typeof BaseScormValidationError} errorClass - The error class to use for validation errors
15178
+ * @param {boolean} allowEmptyString - When true, an empty string is accepted (clears the value).
15179
+ * SCORM 1.2 score elements may be blank per the ADL 1.2 CTS
15180
+ * (DataModelValidator.checkScoreDecimal treats blank as valid).
15100
15181
  * @return {boolean} - True if validation passes, throws an error otherwise
15101
15182
  */
15102
15183
  validateScore(CMIElement, value, decimalRegex, scoreRange, invalidTypeCode, invalidRangeCode, errorClass) {
15184
+ let allowEmptyString = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : false;
15185
+ if (allowEmptyString && value === "") {
15186
+ return true;
15187
+ }
15103
15188
  return checkValidFormat(CMIElement, value, decimalRegex, invalidTypeCode, errorClass) && (!scoreRange || checkValidRange(CMIElement, value, scoreRange, invalidRangeCode, errorClass));
15104
15189
  }
15105
15190
  /**
@@ -15195,6 +15280,21 @@ ${stackTrace}`);
15195
15280
  __publicField$w(this, "_offlineStorageService");
15196
15281
  __publicField$w(this, "_cmiValueAccessService");
15197
15282
  __publicField$w(this, "_courseId", "");
15283
+ __publicField$w(this, "_pendingCommitCount", 0);
15284
+ /**
15285
+ * Monotonic sequence for commits captured by this API instance. It is
15286
+ * intentionally not reset by reset().
15287
+ */
15288
+ __publicField$w(this, "_commitSequence", 0);
15289
+ __publicField$w(this, "_commitSettleWaiters", []);
15290
+ /**
15291
+ * Canonical paths of every CMI element that has been explicitly assigned a
15292
+ * value via SetValue / loadFromJSON (both funnel through _commonSetCMIValue).
15293
+ * Used by standards that must tell "implemented but never set" apart from a
15294
+ * legitimately empty value when answering GetValue (SCORM 2004 error 403).
15295
+ * Cleared on reset so a fresh SCO attempt starts with nothing "set".
15296
+ */
15297
+ __publicField$w(this, "_setCMIElements", /* @__PURE__ */new Set());
15198
15298
  __publicField$w(this, "startingData");
15199
15299
  __publicField$w(this, "currentState");
15200
15300
  if (new.target === BaseAPI) {
@@ -15341,6 +15441,7 @@ ${stackTrace}`);
15341
15441
  this.lastErrorCode = "0";
15342
15442
  this._eventService.reset();
15343
15443
  this.startingData = {};
15444
+ this._setCMIElements.clear();
15344
15445
  if (this._offlineStorageService) {
15345
15446
  this._offlineStorageService.updateSettings(this.settings);
15346
15447
  if (settings?.courseId) {
@@ -15424,6 +15525,54 @@ ${stackTrace}`);
15424
15525
  this._loggingService?.setLogHandler(settings.onLogMessage);
15425
15526
  }
15426
15527
  }
15528
+ /**
15529
+ * Gets the number of captured commit requests that have not yet settled.
15530
+ *
15531
+ * @return {number} The number of in-flight commits
15532
+ */
15533
+ get pendingCommitCount() {
15534
+ return this._pendingCommitCount;
15535
+ }
15536
+ /**
15537
+ * Resolves when all currently in-flight commits have settled. A timeout is
15538
+ * best-effort: the promise resolves when it elapses even if commits remain,
15539
+ * and callers can inspect pendingCommitCount afterward to detect that case.
15540
+ *
15541
+ * @param {Object} [options] - Settle options
15542
+ * @param {number} [options.timeoutMs] - Maximum time to wait in milliseconds
15543
+ * @return {Promise<void>} A promise that resolves after the drain or timeout
15544
+ */
15545
+ whenCommitsSettled(options) {
15546
+ if (this._pendingCommitCount === 0) {
15547
+ return Promise.resolve();
15548
+ }
15549
+ return new Promise(resolve => {
15550
+ const waiter = {
15551
+ resolve
15552
+ };
15553
+ if (options?.timeoutMs !== void 0) {
15554
+ waiter.timeoutId = setTimeout(() => {
15555
+ const waiterIndex = this._commitSettleWaiters.indexOf(waiter);
15556
+ if (waiterIndex === -1) {
15557
+ return;
15558
+ }
15559
+ this._commitSettleWaiters.splice(waiterIndex, 1);
15560
+ resolve();
15561
+ }, options.timeoutMs);
15562
+ }
15563
+ this._commitSettleWaiters.push(waiter);
15564
+ });
15565
+ }
15566
+ /** Resolve and clear every waiter after the pending count reaches zero. */
15567
+ _flushCommitSettleWaiters() {
15568
+ const waiters = this._commitSettleWaiters.splice(0);
15569
+ for (const waiter of waiters) {
15570
+ if (waiter.timeoutId !== void 0) {
15571
+ clearTimeout(waiter.timeoutId);
15572
+ }
15573
+ waiter.resolve();
15574
+ }
15575
+ }
15427
15576
  /**
15428
15577
  * Terminates the current run of the API
15429
15578
  * @param {string} callbackName
@@ -15444,7 +15593,7 @@ ${stackTrace}`);
15444
15593
  } else {
15445
15594
  stateCheckPassed = true;
15446
15595
  this.processListeners("BeforeTerminate");
15447
- const result = this.storeData(true);
15596
+ const result = this.storeData(true, "terminate");
15448
15597
  if ((result.errorCode ?? 0) > 0) {
15449
15598
  if (result.errorMessage) {
15450
15599
  this.apiLog("terminate", `Terminate failed with error: ${result.errorMessage}`, LogLevelEnum.ERROR);
@@ -15484,6 +15633,9 @@ ${stackTrace}`);
15484
15633
  } catch (e) {
15485
15634
  returnValue = this.handleValueAccessException(CMIElement, e, returnValue);
15486
15635
  }
15636
+ if (this.lastErrorCode === "0") {
15637
+ this.checkUninitializedGet(CMIElement, returnValue);
15638
+ }
15487
15639
  this.processListeners(callbackName, CMIElement);
15488
15640
  }
15489
15641
  this.apiLog(callbackName, ": returned: " + returnValue, LogLevelEnum.INFO, CMIElement);
@@ -15493,6 +15645,10 @@ ${stackTrace}`);
15493
15645
  if (this.lastErrorCode === "0") {
15494
15646
  this.clearSCORMError(returnValue);
15495
15647
  }
15648
+ const rawReturn = returnValue;
15649
+ if (typeof rawReturn === "number" || typeof rawReturn === "boolean") {
15650
+ return String(rawReturn);
15651
+ }
15496
15652
  return returnValue;
15497
15653
  }
15498
15654
  /**
@@ -15536,10 +15692,12 @@ ${stackTrace}`);
15536
15692
  * Orders LMS to store all content parameters
15537
15693
  * @param {string} callbackName
15538
15694
  * @param {boolean} checkTerminated
15695
+ * @param {CommitTrigger} trigger - What initiated the commit
15539
15696
  * @return {string}
15540
15697
  */
15541
15698
  commit(callbackName) {
15542
15699
  let checkTerminated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
15700
+ let trigger = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "manual";
15543
15701
  this.clearScheduledCommit();
15544
15702
  let returnValue = global_constants.SCORM_TRUE;
15545
15703
  if (this.isNotInitialized()) {
@@ -15551,20 +15709,21 @@ ${stackTrace}`);
15551
15709
  this.throwSCORMError("api", errorCode);
15552
15710
  if (errorCode === 143) returnValue = global_constants.SCORM_FALSE;
15553
15711
  } else {
15554
- const result = this.storeData(false);
15555
- if ((result.errorCode ?? 0) > 0) {
15712
+ const result = this.storeData(false, trigger);
15713
+ const errorCode = result.errorCode ?? 0;
15714
+ if (errorCode > 0) {
15556
15715
  if (result.errorMessage) {
15557
15716
  this.apiLog("commit", `Commit failed with error: ${result.errorMessage}`, LogLevelEnum.ERROR);
15558
15717
  }
15559
15718
  if (result.errorDetails) {
15560
15719
  this.apiLog("commit", `Error details: ${JSON.stringify(result.errorDetails)}`, LogLevelEnum.DEBUG);
15561
15720
  }
15562
- this.throwSCORMError("api", result.errorCode);
15721
+ this.throwSCORMError("api", errorCode);
15563
15722
  }
15564
15723
  const resultValue = result?.result ?? global_constants.SCORM_FALSE;
15565
15724
  returnValue = typeof resultValue === "boolean" ? String(resultValue) : resultValue;
15566
15725
  this.apiLog(callbackName, " Result: " + returnValue, LogLevelEnum.DEBUG, "HttpRequest");
15567
- if (checkTerminated) this.lastErrorCode = "0";
15726
+ if (checkTerminated && errorCode === 0) this.lastErrorCode = "0";
15568
15727
  this.processListeners(callbackName);
15569
15728
  if (this.settings.enableOfflineSupport && this._offlineStorageService && this._offlineStorageService.isDeviceOnline() && this._courseId) {
15570
15729
  this._offlineStorageService.hasPendingOfflineData(this._courseId).then(hasPendingData => {
@@ -15771,7 +15930,11 @@ ${stackTrace}`);
15771
15930
  * @return {string}
15772
15931
  */
15773
15932
  _commonSetCMIValue(methodName, scorm2004, CMIElement, value) {
15774
- return this._cmiValueAccessService.setCMIValue(methodName, scorm2004, CMIElement, value);
15933
+ const result = this._cmiValueAccessService.setCMIValue(methodName, scorm2004, CMIElement, value);
15934
+ if (result === global_constants.SCORM_TRUE) {
15935
+ this._setCMIElements.add(CMIElement);
15936
+ }
15937
+ return result;
15775
15938
  }
15776
15939
  /**
15777
15940
  * Gets a value from the CMI Object.
@@ -15785,6 +15948,17 @@ ${stackTrace}`);
15785
15948
  _commonGetCMIValue(methodName, scorm2004, CMIElement) {
15786
15949
  return this._cmiValueAccessService.getCMIValue(methodName, scorm2004, CMIElement);
15787
15950
  }
15951
+ /**
15952
+ * Hook invoked by getValue after a successful resolution. Standards that must
15953
+ * distinguish "implemented but never set, no default value" from a
15954
+ * legitimately empty value override this to raise VALUE_NOT_INITIALIZED.
15955
+ * Default is a no-op, so SCORM 1.2 / AICC keep returning "" with code 0.
15956
+ *
15957
+ * @param {string} _CMIElement - the element that was read
15958
+ * @param {any} _returnValue - the value getCMIValue resolved
15959
+ * @protected
15960
+ */
15961
+ checkUninitializedGet(_CMIElement, _returnValue) {}
15788
15962
  /**
15789
15963
  * Returns true if the API's current state is STATE_INITIALIZED
15790
15964
  *
@@ -15867,9 +16041,14 @@ ${stackTrace}`);
15867
16041
  * @param {string} functionName - The name of the function/event that occurred
15868
16042
  * @param {string} CMIElement - Optional CMI element involved in the event
15869
16043
  * @param {any} value - Optional value associated with the event
16044
+ * @param {CommitEventContext} context - Optional context for commit lifecycle events
15870
16045
  */
15871
- processListeners(functionName, CMIElement, value) {
15872
- this._eventService.processListeners(functionName, CMIElement, value);
16046
+ processListeners(functionName, CMIElement, value, context) {
16047
+ if (context !== void 0) {
16048
+ this._eventService.processListeners(functionName, CMIElement, value, context);
16049
+ } else {
16050
+ this._eventService.processListeners(functionName, CMIElement, value);
16051
+ }
15873
16052
  }
15874
16053
  /**
15875
16054
  * Throws a SCORM error with the specified error number and optional message.
@@ -15991,23 +16170,97 @@ ${stackTrace}`);
15991
16170
  * @param {string} url - The URL to send the request to
15992
16171
  * @param {CommitObject | StringKeyMap | Array<any>} params - The parameters to send
15993
16172
  * @param {boolean} immediate - Whether to send the request immediately without waiting
16173
+ * @param {CommitTrigger} [trigger] - What initiated the commit
15994
16174
  * @returns {ResultObject} - The result of the request
15995
16175
  */
15996
16176
  processHttpRequest(url, params) {
15997
16177
  let immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
15998
- if (this.settings.enableOfflineSupport && this._offlineStorageService && !this._offlineStorageService.isDeviceOnline() && this._courseId) {
15999
- this.apiLog("processHttpRequest", "Device is offline, storing data locally", LogLevelEnum.INFO);
16000
- if (params && typeof params === "object" && "cmi" in params) {
16001
- return this._offlineStorageService.storeOffline(this._courseId, params);
16002
- } else {
16003
- this.apiLog("processHttpRequest", "Invalid commit data format for offline storage", LogLevelEnum.ERROR);
16004
- return {
16005
- result: global_constants.SCORM_FALSE,
16006
- errorCode: this._error_codes.GENERAL ?? 101
16007
- };
16178
+ let trigger = arguments.length > 3 ? arguments[3] : undefined;
16179
+ const sequence = ++this._commitSequence;
16180
+ this._pendingCommitCount += 1;
16181
+ let settled = false;
16182
+ let completionDeferred = false;
16183
+ const settle = () => {
16184
+ if (settled) {
16185
+ return;
16186
+ }
16187
+ settled = true;
16188
+ this._pendingCommitCount -= 1;
16189
+ if (this._pendingCommitCount === 0) {
16190
+ this._flushCommitSettleWaiters();
16191
+ }
16192
+ };
16193
+ try {
16194
+ const resolvedTrigger = trigger ?? (immediate ? "terminate" : "manual");
16195
+ let finalParams = params;
16196
+ if (immediate && this.settings.terminateCommitPayloadField) {
16197
+ const field = this.settings.terminateCommitPayloadField;
16198
+ if (Array.isArray(finalParams)) {
16199
+ finalParams = [...finalParams, `${encodeURIComponent(field)}=true`];
16200
+ } else if (finalParams && typeof finalParams === "object") {
16201
+ finalParams = {
16202
+ ...finalParams,
16203
+ [field]: true
16204
+ };
16205
+ }
16206
+ }
16207
+ if (this.settings.includeCommitSequence === true) {
16208
+ if (Array.isArray(finalParams)) {
16209
+ finalParams = [...finalParams, `commitSequence=${sequence}`];
16210
+ } else if (finalParams && typeof finalParams === "object") {
16211
+ finalParams = {
16212
+ ...finalParams,
16213
+ commitSequence: sequence
16214
+ };
16215
+ }
16216
+ }
16217
+ const finalUrl = immediate && this.settings.terminateCommitParam ? appendQueryParam(url, this.settings.terminateCommitParam, "true") : url;
16218
+ const metadata = {
16219
+ isTerminateCommit: immediate,
16220
+ trigger: resolvedTrigger,
16221
+ sequence
16222
+ };
16223
+ const context = {
16224
+ url: finalUrl,
16225
+ trigger: resolvedTrigger,
16226
+ isTerminateCommit: immediate,
16227
+ sequence
16228
+ };
16229
+ if (this.settings.enableOfflineSupport && this._offlineStorageService && !this._offlineStorageService.isDeviceOnline() && this._courseId) {
16230
+ this.apiLog("processHttpRequest", "Device is offline, storing data locally", LogLevelEnum.INFO);
16231
+ if (finalParams && typeof finalParams === "object" && "cmi" in finalParams) {
16232
+ return this._offlineStorageService.storeOffline(this._courseId, finalParams, {
16233
+ isTerminateCommit: immediate,
16234
+ sequence
16235
+ });
16236
+ } else {
16237
+ this.apiLog("processHttpRequest", "Invalid commit data format for offline storage", LogLevelEnum.ERROR);
16238
+ return {
16239
+ result: global_constants.SCORM_FALSE,
16240
+ errorCode: this._error_codes.GENERAL ?? 101
16241
+ };
16242
+ }
16243
+ }
16244
+ const apiLog = (functionName, message, level, element) => this.apiLog(functionName, message, level, element);
16245
+ const processListeners = (functionName, CMIElement, value) => {
16246
+ if (functionName === "CommitSuccess" || functionName === "CommitError") {
16247
+ if (functionName === "CommitError" && typeof value === "number") {
16248
+ context.errorCode = value;
16249
+ }
16250
+ settle();
16251
+ this.processListeners(functionName, CMIElement, value, context);
16252
+ } else {
16253
+ this.processListeners(functionName, CMIElement, value);
16254
+ }
16255
+ };
16256
+ const result = this._httpService.processHttpRequest(finalUrl, finalParams, immediate, apiLog, processListeners, metadata, settle);
16257
+ completionDeferred = this._httpService.reportsRequestCompletion === true;
16258
+ return result;
16259
+ } finally {
16260
+ if (!completionDeferred) {
16261
+ settle();
16008
16262
  }
16009
16263
  }
16010
- 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));
16011
16264
  }
16012
16265
  /**
16013
16266
  * Schedules a commit operation to occur after a specified delay.
@@ -16191,6 +16444,11 @@ ${stackTrace}`);
16191
16444
  __publicField$v(this, "__invalid_range_code");
16192
16445
  __publicField$v(this, "__decimal_regex");
16193
16446
  __publicField$v(this, "__error_class");
16447
+ /**
16448
+ * When true, an empty string is a valid value (clears the element). SCORM 1.2
16449
+ * score elements may be blank per the ADL 1.2 CTS; SCORM 2004 leaves this off.
16450
+ */
16451
+ __publicField$v(this, "__allow_empty_string");
16194
16452
  __publicField$v(this, "_raw", "");
16195
16453
  __publicField$v(this, "_min", "");
16196
16454
  __publicField$v(this, "_max");
@@ -16202,6 +16460,7 @@ ${stackTrace}`);
16202
16460
  this.__invalid_range_code = params.invalidRangeCode || scorm12_errors.VALUE_OUT_OF_RANGE;
16203
16461
  this.__decimal_regex = params.decimalRegex || scorm12_regex.CMIDecimal;
16204
16462
  this.__error_class = params.errorClass;
16463
+ this.__allow_empty_string = params.allowEmptyString ?? false;
16205
16464
  }
16206
16465
  /**
16207
16466
  * Called when the API has been reset
@@ -16241,7 +16500,7 @@ ${stackTrace}`);
16241
16500
  * @param {string} raw
16242
16501
  */
16243
16502
  set raw(raw) {
16244
- 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)) {
16503
+ 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)) {
16245
16504
  this._raw = raw;
16246
16505
  }
16247
16506
  }
@@ -16257,7 +16516,7 @@ ${stackTrace}`);
16257
16516
  * @param {string} min
16258
16517
  */
16259
16518
  set min(min) {
16260
- 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)) {
16519
+ 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)) {
16261
16520
  this._min = min;
16262
16521
  }
16263
16522
  }
@@ -16273,7 +16532,7 @@ ${stackTrace}`);
16273
16532
  * @param {string} max
16274
16533
  */
16275
16534
  set max(max) {
16276
- 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)) {
16535
+ 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)) {
16277
16536
  this._max = max;
16278
16537
  }
16279
16538
  }
@@ -16350,7 +16609,9 @@ ${stackTrace}`);
16350
16609
  invalidErrorCode: scorm12_errors.INVALID_SET_VALUE,
16351
16610
  invalidTypeCode: scorm12_errors.TYPE_MISMATCH,
16352
16611
  invalidRangeCode: scorm12_errors.VALUE_OUT_OF_RANGE,
16353
- errorClass: Scorm12ValidationError
16612
+ errorClass: Scorm12ValidationError,
16613
+ // SCORM 1.2 score elements may be set blank (clears the value), per ADL 1.2 CTS.
16614
+ allowEmptyString: true
16354
16615
  });
16355
16616
  }
16356
16617
  /**
@@ -16718,7 +16979,9 @@ ${stackTrace}`);
16718
16979
  invalidErrorCode: scorm12_errors.INVALID_SET_VALUE,
16719
16980
  invalidTypeCode: scorm12_errors.TYPE_MISMATCH,
16720
16981
  invalidRangeCode: scorm12_errors.VALUE_OUT_OF_RANGE,
16721
- errorClass: Scorm12ValidationError
16982
+ errorClass: Scorm12ValidationError,
16983
+ // SCORM 1.2 score elements may be set blank (clears the value), per ADL 1.2 CTS.
16984
+ allowEmptyString: true
16722
16985
  });
16723
16986
  }
16724
16987
  /**
@@ -18155,16 +18418,17 @@ ${stackTrace}`);
18155
18418
  * Attempts to store the data to the LMS
18156
18419
  *
18157
18420
  * @param {boolean} terminateCommit
18421
+ * @param {CommitTrigger} [trigger] - What initiated the commit
18158
18422
  * @return {ResultObject}
18159
18423
  */
18160
- storeData(terminateCommit) {
18424
+ storeData(terminateCommit, trigger) {
18161
18425
  if (terminateCommit) {
18162
18426
  const originalStatus = this.cmi.core.lesson_status;
18163
18427
  if (this.cmi.core.lesson_mode === "browse") {
18164
18428
  const startingStatus = this.startingData?.cmi?.core?.lesson_status || "";
18165
18429
  if (startingStatus === "" && originalStatus === "not attempted") {
18166
18430
  this.cmi.core.lesson_status = "browsed";
18167
- return this.processCommitData(terminateCommit);
18431
+ return this.processCommitData(terminateCommit, trigger);
18168
18432
  }
18169
18433
  }
18170
18434
  if (!this.cmi.core.lesson_status || !this.statusSetByModule && this.cmi.core.lesson_status === "not attempted") {
@@ -18193,12 +18457,12 @@ ${stackTrace}`);
18193
18457
  }
18194
18458
  }
18195
18459
  }
18196
- return this.processCommitData(terminateCommit);
18460
+ return this.processCommitData(terminateCommit, trigger);
18197
18461
  }
18198
- processCommitData(terminateCommit) {
18462
+ processCommitData(terminateCommit, trigger) {
18199
18463
  const commitObject = this.getCommitObject(terminateCommit);
18200
18464
  if (typeof this.settings.lmsCommitUrl === "string") {
18201
- return this.processHttpRequest(this.settings.lmsCommitUrl, commitObject, terminateCommit);
18465
+ return this.processHttpRequest(this.settings.lmsCommitUrl, commitObject, terminateCommit, trigger);
18202
18466
  } else {
18203
18467
  return {
18204
18468
  result: global_constants.SCORM_TRUE,
@@ -18349,6 +18613,49 @@ ${stackTrace}`);
18349
18613
  }
18350
18614
  }
18351
18615
 
18616
+ function stripBrackets(delim) {
18617
+ return delim.replace(/[[\]]/g, "");
18618
+ }
18619
+ function escapeRegex(s) {
18620
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18621
+ }
18622
+ function splitDelimited(value, bracketed) {
18623
+ if (!bracketed) {
18624
+ return [value];
18625
+ }
18626
+ if (value.includes(bracketed)) {
18627
+ return value.split(bracketed);
18628
+ }
18629
+ const bare = stripBrackets(bracketed);
18630
+ if (!bare) {
18631
+ return [value];
18632
+ }
18633
+ const splitRe = new RegExp(`(?<!\\\\)${escapeRegex(bare)}`, "g");
18634
+ const unescapeRe = new RegExp(`\\\\${escapeRegex(bare)}`, "g");
18635
+ return value.split(splitRe).map(part => part.replace(unescapeRe, bare));
18636
+ }
18637
+ function splitFirstDelimited(value, bracketed) {
18638
+ if (!bracketed) {
18639
+ return [value];
18640
+ }
18641
+ if (value.includes(bracketed)) {
18642
+ const idx = value.indexOf(bracketed);
18643
+ return [value.slice(0, idx), value.slice(idx + bracketed.length)];
18644
+ }
18645
+ const bare = stripBrackets(bracketed);
18646
+ if (!bare) {
18647
+ return [value];
18648
+ }
18649
+ const splitRe = new RegExp(`(?<!\\\\)${escapeRegex(bare)}`);
18650
+ const unescapeRe = new RegExp(`\\\\${escapeRegex(bare)}`, "g");
18651
+ const parts = value.split(splitRe);
18652
+ const first = (parts[0] ?? "").replace(unescapeRe, bare);
18653
+ if (parts.length === 1) {
18654
+ return [first];
18655
+ }
18656
+ return [first, parts.slice(1).join(bare).replace(unescapeRe, bare)];
18657
+ }
18658
+
18352
18659
  var __defProp$l = Object.defineProperty;
18353
18660
  var __defNormalProp$l = (obj, key, value) => key in obj ? __defProp$l(obj, key, {
18354
18661
  enumerable: true,
@@ -18544,8 +18851,7 @@ ${stackTrace}`);
18544
18851
  const response_type = LearnerResponses[this.type];
18545
18852
  if (response_type) {
18546
18853
  if (response_type?.delimiter) {
18547
- const delimiter = response_type.delimiter === "[,]" ? "," : response_type.delimiter;
18548
- nodes = learner_response.split(delimiter);
18854
+ nodes = splitDelimited(learner_response, response_type.delimiter);
18549
18855
  } else {
18550
18856
  nodes[0] = learner_response;
18551
18857
  }
@@ -18553,10 +18859,10 @@ ${stackTrace}`);
18553
18859
  const formatRegex = new RegExp(response_type.format);
18554
18860
  for (let i = 0; i < nodes.length; i++) {
18555
18861
  if (response_type?.delimiter2) {
18556
- const delimiter2 = response_type.delimiter2 === "[.]" ? "." : response_type.delimiter2;
18557
- const values = nodes[i]?.split(delimiter2);
18862
+ const node = nodes[i] ?? "";
18863
+ const values = this.type === "performance" ? splitFirstDelimited(node, response_type.delimiter2) : splitDelimited(node, response_type.delimiter2);
18558
18864
  if (values?.length === 2) {
18559
- if (this.type === "performance" && (values[0] === "" || values[1] === "")) {
18865
+ if (this.type === "performance" && values[0] === "" && values[1] === "") {
18560
18866
  throw new Scorm2004ValidationError(this._cmi_element + ".learner_response", scorm2004_errors.TYPE_MISMATCH);
18561
18867
  }
18562
18868
  if (!values[0]?.match(formatRegex)) {
@@ -18737,37 +19043,19 @@ ${stackTrace}`);
18737
19043
  return result;
18738
19044
  }
18739
19045
  }
18740
- function stripBrackets(delim) {
18741
- return delim.replace(/[[\]]/g, "");
18742
- }
18743
- function escapeRegex(s) {
18744
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18745
- }
18746
- function splitUnescaped(text, delim) {
18747
- const reDelim = escapeRegex(delim);
18748
- const splitRe = new RegExp(`(?<!\\\\)${reDelim}`, "g");
18749
- const unescapeRe = new RegExp(`\\\\${reDelim}`, "g");
18750
- return text.split(splitRe).map(part => part.replace(unescapeRe, delim));
18751
- }
18752
- function splitFirstUnescaped(text, delim) {
18753
- const reDelim = escapeRegex(delim);
18754
- const splitRe = new RegExp(`(?<!\\\\)${reDelim}`);
18755
- const unescapeRe = new RegExp(`\\\\${reDelim}`, "g");
18756
- const parts = text.split(splitRe);
18757
- const firstPart = parts[0] ?? "";
18758
- if (parts.length === 1) {
18759
- return [firstPart.replace(unescapeRe, delim)];
19046
+ const RESPONSE_PREFIX_RE = /^\{(?:lang|case_matters|order_matters)=[^}]+\}/;
19047
+ function stripResponsePrefixes(node) {
19048
+ let result = node;
19049
+ while (RESPONSE_PREFIX_RE.test(result)) {
19050
+ result = result.replace(RESPONSE_PREFIX_RE, "");
18760
19051
  }
18761
- const part1 = firstPart.replace(unescapeRe, delim);
18762
- const part2 = parts.slice(1).join(delim).replace(unescapeRe, delim);
18763
- return [part1, part2];
19052
+ return result;
18764
19053
  }
18765
19054
  function validatePattern(type, pattern, responseDef) {
18766
19055
  if (pattern.trim() !== pattern) {
18767
19056
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
18768
19057
  }
18769
- const subDelim1 = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : null;
18770
- const rawNodes = subDelim1 ? splitUnescaped(pattern, subDelim1) : [pattern];
19058
+ const rawNodes = responseDef.delimiter ? splitDelimited(pattern, responseDef.delimiter) : [pattern];
18771
19059
  for (const raw of rawNodes) {
18772
19060
  if (raw.trim() !== raw) {
18773
19061
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
@@ -18776,17 +19064,11 @@ ${stackTrace}`);
18776
19064
  if (type === "fill-in" && pattern === "") {
18777
19065
  return;
18778
19066
  }
18779
- const delim1 = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : null;
18780
- let nodes;
18781
- if (delim1) {
18782
- nodes = splitUnescaped(pattern, delim1);
18783
- } else {
18784
- nodes = [pattern];
18785
- }
19067
+ const nodes = responseDef.delimiter ? splitDelimited(pattern, responseDef.delimiter) : [pattern];
18786
19068
  if (!responseDef.delimiter && pattern.includes(",")) {
18787
19069
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
18788
19070
  }
18789
- if (responseDef.unique || responseDef.duplicate === false) {
19071
+ if (type !== "numeric" && (responseDef.unique || responseDef.duplicate === false)) {
18790
19072
  const seen = new Set(nodes);
18791
19073
  if (seen.size !== nodes.length) {
18792
19074
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
@@ -18806,8 +19088,7 @@ ${stackTrace}`);
18806
19088
  if (!delimBracketed) {
18807
19089
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
18808
19090
  }
18809
- const delim = stripBrackets(delimBracketed);
18810
- const parts = value.split(new RegExp(`(?<!\\\\)${escapeRegex(delim)}`, "g")).map(n => n.replace(new RegExp(`\\\\${escapeRegex(delim)}`, "g"), delim));
19091
+ const parts = splitDelimited(value, delimBracketed);
18811
19092
  if (parts.length !== 2 || parts[0] === "" || parts[1] === "") {
18812
19093
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
18813
19094
  }
@@ -18819,12 +19100,14 @@ ${stackTrace}`);
18819
19100
  switch (type) {
18820
19101
  case "numeric":
18821
19102
  {
18822
- const numDelim = responseDef.delimiter ? stripBrackets(responseDef.delimiter) : ":";
18823
- const nums = node.split(numDelim);
18824
- if (nums.length < 1 || nums.length > 2) {
18825
- throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
19103
+ if (node === "") {
19104
+ const bracketedRange = nodes.length >= 2 && !!responseDef.delimiter && pattern.includes(responseDef.delimiter);
19105
+ if (!bracketedRange) {
19106
+ throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
19107
+ }
19108
+ break;
18826
19109
  }
18827
- nums.forEach(checkSingle);
19110
+ checkSingle(node);
18828
19111
  break;
18829
19112
  }
18830
19113
  case "performance":
@@ -18833,13 +19116,13 @@ ${stackTrace}`);
18833
19116
  if (!delimBracketed) {
18834
19117
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
18835
19118
  }
18836
- const delim = stripBrackets(delimBracketed);
18837
- const parts = splitFirstUnescaped(node, delim);
19119
+ const record = stripResponsePrefixes(node);
19120
+ const parts = splitFirstDelimited(record, delimBracketed);
18838
19121
  if (parts.length !== 2) {
18839
19122
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
18840
19123
  }
18841
19124
  const [part1, part2] = parts;
18842
- if (part1 === "" || part2 === "" || part1 === part2) {
19125
+ if (part1 === "" && part2 === "") {
18843
19126
  throw new Scorm2004ValidationError("cmi.interactions.n.correct_responses.n.pattern", scorm2004_errors.TYPE_MISMATCH);
18844
19127
  }
18845
19128
  if (part1 === void 0 || !fmt1.test(part1)) {
@@ -21625,7 +21908,7 @@ ${stackTrace}`);
21625
21908
  checkValidResponseType(CMIElement, response_type, value, interaction_type) {
21626
21909
  let nodes = [];
21627
21910
  if (response_type?.delimiter) {
21628
- nodes = String(value).split(response_type.delimiter);
21911
+ nodes = splitDelimited(String(value), response_type.delimiter);
21629
21912
  } else {
21630
21913
  nodes[0] = value;
21631
21914
  }
@@ -21723,26 +22006,33 @@ ${stackTrace}`);
21723
22006
  nodes[i] = this.removeCorrectResponsePrefixes(CMIElement, nodes[i]);
21724
22007
  }
21725
22008
  if (response?.delimiter2) {
21726
- const values = nodes[i].split(response.delimiter2);
22009
+ const values = interaction_type === "performance" ? splitFirstDelimited(nodes[i], response.delimiter2) : splitDelimited(nodes[i], response.delimiter2);
21727
22010
  if (values.length === 2) {
21728
- const matches = values[0].match(formatRegex);
21729
- if (!matches) {
22011
+ if (interaction_type === "performance" && values[0] === "" && values[1] === "") {
21730
22012
  this.context.throwSCORMError(CMIElement, scorm2004_errors.TYPE_MISMATCH, `${interaction_type}: ${value}`);
21731
22013
  } else {
21732
- if (!response.format2 || !values[1].match(new RegExp(response.format2))) {
22014
+ const matches = values[0]?.match(formatRegex);
22015
+ if (!matches) {
21733
22016
  this.context.throwSCORMError(CMIElement, scorm2004_errors.TYPE_MISMATCH, `${interaction_type}: ${value}`);
22017
+ } else {
22018
+ if (!response.format2 || !values[1]?.match(new RegExp(response.format2))) {
22019
+ this.context.throwSCORMError(CMIElement, scorm2004_errors.TYPE_MISMATCH, `${interaction_type}: ${value}`);
22020
+ }
21734
22021
  }
21735
22022
  }
21736
22023
  } else {
21737
22024
  this.context.throwSCORMError(CMIElement, scorm2004_errors.TYPE_MISMATCH, `${interaction_type}: ${value}`);
21738
22025
  }
21739
22026
  } else {
22027
+ if (interaction_type === "numeric" && nodes.length > 1 && nodes[i] === "" && !!response.delimiter && String(value).includes(response.delimiter)) {
22028
+ continue;
22029
+ }
21740
22030
  const matches = nodes[i].match(formatRegex);
21741
22031
  if (!matches && value !== "" || !matches && interaction_type === "true-false") {
21742
22032
  this.context.throwSCORMError(CMIElement, scorm2004_errors.TYPE_MISMATCH, `${interaction_type}: ${value}`);
21743
22033
  } else {
21744
22034
  if (interaction_type === "numeric" && nodes.length > 1) {
21745
- if (Number(nodes[0]) > Number(nodes[1])) {
22035
+ if (nodes[0] !== "" && nodes[1] !== "" && Number(nodes[0]) > Number(nodes[1])) {
21746
22036
  this.context.throwSCORMError(CMIElement, scorm2004_errors.TYPE_MISMATCH, `${interaction_type}: ${value}`);
21747
22037
  }
21748
22038
  } else {
@@ -23421,6 +23711,10 @@ ${stackTrace}`);
23421
23711
  value
23422
23712
  }) : obj[key] = value;
23423
23713
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
23714
+ 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"]);
23715
+ function normalizeCMIIndices(CMIElement) {
23716
+ return CMIElement.replace(/\.\d+(?=\.|$)/g, ".N");
23717
+ }
23424
23718
  class Scorm2004API extends BaseAPI {
23425
23719
  /**
23426
23720
  * Constructor for SCORM 2004 API
@@ -23920,6 +24214,35 @@ ${stackTrace}`);
23920
24214
  getCMIValue(CMIElement) {
23921
24215
  return this._commonGetCMIValue("GetValue", true, CMIElement);
23922
24216
  }
24217
+ /**
24218
+ * Raises 403 (VALUE_NOT_INITIALIZED) when an implemented, no-default element is
24219
+ * read before it has ever been set, per IEEE 1484.11.2 / SCORM 2004 RTE.
24220
+ *
24221
+ * Invoked by BaseAPI.getValue after an otherwise-successful resolution, so it
24222
+ * only sees values the data model already returned cleanly. The decision is
24223
+ * deliberately gated to this public boundary (never the getters) to keep
24224
+ * serialization/commit/rollup — which read the getters directly — unaffected.
24225
+ *
24226
+ * A 403 is raised only when all hold:
24227
+ * - the resolved value is "" — any non-empty value is by definition set,
24228
+ * including values written by internal sequencing/activity-tree paths that
24229
+ * bypass _commonSetCMIValue (those only ever write non-empty values);
24230
+ * - the element was never set — _setCMIElements records every successful
24231
+ * SetValue, loadFromJSON, and global-objective restore (all route through
24232
+ * _commonSetCMIValue), so an explicit SetValue(x, "") still reads back as
24233
+ * "" with code 0; and
24234
+ * - the element has no spec-defined default ({@link NO_DEFAULT_2004_ELEMENTS}).
24235
+ *
24236
+ * @param {string} CMIElement
24237
+ * @param {*} returnValue - the value getCMIValue resolved
24238
+ * @protected
24239
+ */
24240
+ checkUninitializedGet(CMIElement, returnValue) {
24241
+ if (returnValue !== "") return;
24242
+ if (this._setCMIElements.has(CMIElement)) return;
24243
+ if (!NO_DEFAULT_2004_ELEMENTS.has(normalizeCMIIndices(CMIElement))) return;
24244
+ this.throwSCORMError(CMIElement, this._error_codes.VALUE_NOT_INITIALIZED ?? 403, `The data model element passed to GetValue (${CMIElement}) has not been initialized.`);
24245
+ }
23923
24246
  /**
23924
24247
  * Returns the message that corresponds to errorNumber.
23925
24248
  * @param {(string|number)} errorNumber
@@ -23968,9 +24291,10 @@ ${stackTrace}`);
23968
24291
  /**
23969
24292
  * Attempts to store the data to the LMS
23970
24293
  * @param {boolean} terminateCommit
24294
+ * @param {CommitTrigger} [trigger] - What initiated the commit
23971
24295
  * @return {ResultObject}
23972
24296
  */
23973
- storeData(terminateCommit) {
24297
+ storeData(terminateCommit, trigger) {
23974
24298
  if (terminateCommit) {
23975
24299
  if (this.cmi.mode === "normal") {
23976
24300
  if (this.cmi.credit === "credit") {
@@ -24011,7 +24335,7 @@ ${stackTrace}`);
24011
24335
  }
24012
24336
  this._globalObjectiveManager.syncCmiToSequencingActivity(completionStatusEnum, successStatusEnum, scoreObject);
24013
24337
  if (typeof this.settings.lmsCommitUrl === "string") {
24014
- const result = this.processHttpRequest(this.settings.lmsCommitUrl, commitObject, terminateCommit);
24338
+ const result = this.processHttpRequest(this.settings.lmsCommitUrl, commitObject, terminateCommit, trigger);
24015
24339
  if (navRequest && result.navRequest !== void 0 && result.navRequest !== "" && typeof result.navRequest === "string") {
24016
24340
  const parsed = parseNavigationRequest(result.navRequest);
24017
24341
  if (!parsed.valid) {