scorm-again 3.0.5 → 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 (41) hide show
  1. package/README.md +77 -1
  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 +388 -71
  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 +322 -69
  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 +371 -64
  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 +341 -55
  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 +305 -53
  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 +329 -48
  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/interfaces/services.d.ts +8 -4
  33. package/dist/types/services/AsynchronousHttpService.d.ts +3 -2
  34. package/dist/types/services/EventService.d.ts +2 -2
  35. package/dist/types/services/OfflineStorageService.d.ts +6 -1
  36. package/dist/types/services/SynchronousHttpService.d.ts +2 -2
  37. package/dist/types/services/ValidationService.d.ts +1 -1
  38. package/dist/types/types/api_types.d.ts +22 -2
  39. package/dist/types/utilities/index.d.ts +1 -0
  40. package/dist/types/utilities/url.d.ts +2 -0
  41. package/package.json +21 -21
@@ -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;
@@ -1354,7 +1367,7 @@
1354
1367
  wrapper() {
1355
1368
  if (!this._cancelled) {
1356
1369
  if (this._API.isInitialized()) {
1357
- (async () => await this._API.commit(this._callback))();
1370
+ (async () => await this._API.commit(this._callback, false, "autocommit"))();
1358
1371
  }
1359
1372
  }
1360
1373
  }
@@ -4827,6 +4840,7 @@
4827
4840
  * @param {ErrorCode} error_codes - The error codes object
4828
4841
  */
4829
4842
  constructor(settings, error_codes) {
4843
+ __publicField$Y(this, "reportsRequestCompletion", true);
4830
4844
  __publicField$Y(this, "settings");
4831
4845
  __publicField$Y(this, "error_codes");
4832
4846
  this.settings = settings;
@@ -4845,13 +4859,17 @@
4845
4859
  * @param {boolean} immediate - Whether to send the request immediately without waiting
4846
4860
  * @param {Function} apiLog - Function to log API messages with appropriate levels
4847
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
4848
4864
  * @return {ResultObject} - Immediate optimistic success result
4849
4865
  */
4850
4866
  processHttpRequest(url, params) {
4851
4867
  let immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
4852
4868
  let apiLog = arguments.length > 3 ? arguments[3] : undefined;
4853
4869
  let processListeners = arguments.length > 4 ? arguments[4] : undefined;
4854
- 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);
4855
4873
  return {
4856
4874
  result: global_constants.SCORM_TRUE,
4857
4875
  errorCode: 0
@@ -4864,11 +4882,14 @@
4864
4882
  * @param {boolean} immediate - Whether this is an immediate request
4865
4883
  * @param apiLog - Function to log API messages
4866
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
4867
4887
  * @private
4868
4888
  */
4869
- async _performAsyncRequest(url, params, immediate, apiLog, processListeners) {
4889
+ async _performAsyncRequest(url, params, immediate, apiLog, processListeners, metadata, onRequestComplete) {
4870
4890
  try {
4871
- 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;
4872
4893
  let response;
4873
4894
  if (immediate && this.settings.asyncModeBeaconBehavior !== "never") {
4874
4895
  response = await this.performBeacon(url, processedParams);
@@ -4884,7 +4905,9 @@
4884
4905
  } catch (e) {
4885
4906
  const message = e instanceof Error ? e.message : String(e);
4886
4907
  apiLog("processHttpRequest", `Async request failed: ${message}`, LogLevelEnum.ERROR);
4887
- processListeners("CommitError");
4908
+ processListeners("CommitError", void 0, this.error_codes.GENERAL_COMMIT_FAILURE || 391);
4909
+ } finally {
4910
+ onRequestComplete?.();
4888
4911
  }
4889
4912
  }
4890
4913
  /**
@@ -5046,10 +5069,13 @@
5046
5069
  }
5047
5070
  /**
5048
5071
  * Gets the appropriate error code for undefined data model elements.
5049
- * 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.
5050
5076
  */
5051
- getUndefinedDataModelErrorCode(scorm2004) {
5052
- 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");
5053
5079
  }
5054
5080
  /**
5055
5081
  * Sets a value on a CMI element path
@@ -5073,7 +5099,7 @@
5073
5099
  let returnValue = global_constants.SCORM_FALSE;
5074
5100
  let foundFirstIndex = false;
5075
5101
  const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
5076
- const invalidErrorCode = this.getUndefinedDataModelErrorCode(scorm2004);
5102
+ const invalidErrorCode = this.getUndefinedDataModelErrorCode();
5077
5103
  for (let idx = 0; idx < structure.length; idx++) {
5078
5104
  const attribute = structure[idx];
5079
5105
  if (idx === structure.length - 1) {
@@ -5113,12 +5139,13 @@
5113
5139
  this.context.throwSCORMError(CMIElement, getErrorCode(this.context.errorCodes, "GENERAL_GET_FAILURE"), "The _version keyword was used incorrectly");
5114
5140
  return "";
5115
5141
  }
5142
+ this.context.setLastErrorCode("0");
5116
5143
  const structure = CMIElement.split(".");
5117
5144
  let refObject = this.context.getDataModel();
5118
5145
  let attribute = null;
5119
5146
  const uninitializedErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) has not been initialized.`;
5120
5147
  const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
5121
- const invalidErrorCode = this.getUndefinedDataModelErrorCode(scorm2004);
5148
+ const invalidErrorCode = this.getUndefinedDataModelErrorCode();
5122
5149
  for (let idx = 0; idx < structure.length; idx++) {
5123
5150
  attribute = structure[idx];
5124
5151
  const validationResult = this.validateGetAttribute(refObject, attribute, CMIElement, scorm2004, invalidErrorCode, invalidErrorMessage, idx === structure.length - 1);
@@ -5844,8 +5871,9 @@ ${stackTrace}`);
5844
5871
  * @param {string} functionName - The name of the function that triggered the event
5845
5872
  * @param {string} CMIElement - The CMI element that was affected
5846
5873
  * @param {any} value - The value that was set
5874
+ * @param {CommitEventContext} context - Optional context for commit lifecycle events
5847
5875
  */
5848
- processListeners(functionName, CMIElement, value) {
5876
+ processListeners(functionName, CMIElement, value, context) {
5849
5877
  this.apiLog(functionName, value, LogLevelEnum.INFO, CMIElement);
5850
5878
  const listeners = this.listenerMap.get(functionName);
5851
5879
  if (!listeners) return;
@@ -5865,9 +5893,17 @@ ${stackTrace}`);
5865
5893
  if (functionName.startsWith("Sequence")) {
5866
5894
  listener.callback(value);
5867
5895
  } else if (functionName === "CommitError") {
5868
- listener.callback(value);
5896
+ if (context !== void 0) {
5897
+ listener.callback(value, context);
5898
+ } else {
5899
+ listener.callback(value);
5900
+ }
5869
5901
  } else if (functionName === "CommitSuccess") {
5870
- listener.callback();
5902
+ if (context !== void 0) {
5903
+ listener.callback(context);
5904
+ } else {
5905
+ listener.callback();
5906
+ }
5871
5907
  } else {
5872
5908
  listener.callback(CMIElement, value);
5873
5909
  }
@@ -5976,16 +6012,23 @@ ${stackTrace}`);
5976
6012
  * Store commit data offline
5977
6013
  * @param {string} courseId - Identifier for the course
5978
6014
  * @param {CommitObject} commitData - The data to store offline
6015
+ * @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
5979
6016
  * @returns {ResultObject} - Result of the storage operation
5980
6017
  */
5981
- storeOffline(courseId, commitData) {
6018
+ storeOffline(courseId, commitData, metadata) {
5982
6019
  try {
5983
6020
  const queueItem = {
5984
6021
  id: `${courseId}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
5985
6022
  courseId,
5986
6023
  timestamp: Date.now(),
5987
6024
  data: commitData,
5988
- 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
+ } : {})
5989
6032
  };
5990
6033
  const currentQueue = this.getFromStorage(this.syncQueue) || [];
5991
6034
  currentQueue.push(queueItem);
@@ -6044,7 +6087,14 @@ ${stackTrace}`);
6044
6087
  continue;
6045
6088
  }
6046
6089
  try {
6047
- 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
+ });
6048
6098
  if (syncResult.result === true || syncResult.result === global_constants.SCORM_TRUE) {
6049
6099
  this.apiLog("OfflineStorageService", `Successfully synced item ${item.id}`, LogLevelEnum.INFO);
6050
6100
  } else {
@@ -6071,17 +6121,27 @@ ${stackTrace}`);
6071
6121
  /**
6072
6122
  * Send data to the LMS when online
6073
6123
  * @param {CommitObject} data - The data to send to the LMS
6124
+ * @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
6074
6125
  * @returns {Promise<ResultObject>} - Result of the sync operation
6075
6126
  */
6076
- async sendDataToLMS(data) {
6077
- if (!this.settings.lmsCommitUrl) {
6127
+ async sendDataToLMS(data, metadata) {
6128
+ const configuredCommitUrl = this.settings.lmsCommitUrl;
6129
+ if (!configuredCommitUrl) {
6078
6130
  return {
6079
6131
  result: global_constants.SCORM_FALSE,
6080
6132
  errorCode: this.error_codes.GENERAL || 101
6081
6133
  };
6082
6134
  }
6083
6135
  try {
6084
- 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;
6085
6145
  const init = {
6086
6146
  method: "POST",
6087
6147
  mode: this.settings.fetchMode,
@@ -6094,7 +6154,7 @@ ${stackTrace}`);
6094
6154
  if (this.settings.xhrWithCredentials) {
6095
6155
  init.credentials = "include";
6096
6156
  }
6097
- const response = await fetch(this.settings.lmsCommitUrl, init);
6157
+ const response = await fetch(requestUrl, init);
6098
6158
  const result = typeof this.settings.responseHandler === "function" ? await this.settings.responseHandler(response) : await response.json();
6099
6159
  if (response.status >= 200 && response.status <= 299 && (result.result === true || result.result === global_constants.SCORM_TRUE)) {
6100
6160
  if (!Object.hasOwnProperty.call(result, "errorCode")) {
@@ -14991,6 +15051,8 @@ ${stackTrace}`);
14991
15051
  * @param {boolean} immediate - Whether this is a termination commit (use sendBeacon)
14992
15052
  * @param {Function} _apiLog - Function to log API messages (unused in synchronous mode - errors returned directly)
14993
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)
14994
15056
  * @return {ResultObject} - The result of the request (synchronous)
14995
15057
  *
14996
15058
  * @remarks
@@ -15002,20 +15064,23 @@ ${stackTrace}`);
15002
15064
  */
15003
15065
  processHttpRequest(url, params) {
15004
15066
  let immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
15067
+ let metadata = arguments.length > 5 ? arguments[5] : undefined;
15005
15068
  if (immediate) {
15006
- return this._handleImmediateRequest(url, params);
15069
+ return this._handleImmediateRequest(url, params, metadata);
15007
15070
  }
15008
- return this._performSyncXHR(url, params);
15071
+ return this._performSyncXHR(url, params, metadata);
15009
15072
  }
15010
15073
  /**
15011
15074
  * Handles an immediate request using sendBeacon
15012
15075
  * @param {string} url - The URL to send the request to
15013
15076
  * @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
15077
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
15014
15078
  * @return {ResultObject} - The result based on beacon success
15015
15079
  * @private
15016
15080
  */
15017
- _handleImmediateRequest(url, params) {
15018
- 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;
15019
15084
  const {
15020
15085
  body
15021
15086
  } = this._prepareRequestBody(requestPayload);
@@ -15031,11 +15096,13 @@ ${stackTrace}`);
15031
15096
  * Performs a synchronous XMLHttpRequest
15032
15097
  * @param {string} url - The URL to send the request to
15033
15098
  * @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
15099
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
15034
15100
  * @return {ResultObject} - The result of the request
15035
15101
  * @private
15036
15102
  */
15037
- _performSyncXHR(url, params) {
15038
- 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;
15039
15106
  const {
15040
15107
  body,
15041
15108
  contentType
@@ -15108,9 +15175,16 @@ ${stackTrace}`);
15108
15175
  * @param {number} invalidTypeCode - The error code for invalid type
15109
15176
  * @param {number} invalidRangeCode - The error code for invalid range
15110
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).
15111
15181
  * @return {boolean} - True if validation passes, throws an error otherwise
15112
15182
  */
15113
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
+ }
15114
15188
  return checkValidFormat(CMIElement, value, decimalRegex, invalidTypeCode, errorClass) && (!scoreRange || checkValidRange(CMIElement, value, scoreRange, invalidRangeCode, errorClass));
15115
15189
  }
15116
15190
  /**
@@ -15206,6 +15280,21 @@ ${stackTrace}`);
15206
15280
  __publicField$w(this, "_offlineStorageService");
15207
15281
  __publicField$w(this, "_cmiValueAccessService");
15208
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());
15209
15298
  __publicField$w(this, "startingData");
15210
15299
  __publicField$w(this, "currentState");
15211
15300
  if (new.target === BaseAPI) {
@@ -15352,6 +15441,7 @@ ${stackTrace}`);
15352
15441
  this.lastErrorCode = "0";
15353
15442
  this._eventService.reset();
15354
15443
  this.startingData = {};
15444
+ this._setCMIElements.clear();
15355
15445
  if (this._offlineStorageService) {
15356
15446
  this._offlineStorageService.updateSettings(this.settings);
15357
15447
  if (settings?.courseId) {
@@ -15435,6 +15525,54 @@ ${stackTrace}`);
15435
15525
  this._loggingService?.setLogHandler(settings.onLogMessage);
15436
15526
  }
15437
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
+ }
15438
15576
  /**
15439
15577
  * Terminates the current run of the API
15440
15578
  * @param {string} callbackName
@@ -15455,7 +15593,7 @@ ${stackTrace}`);
15455
15593
  } else {
15456
15594
  stateCheckPassed = true;
15457
15595
  this.processListeners("BeforeTerminate");
15458
- const result = this.storeData(true);
15596
+ const result = this.storeData(true, "terminate");
15459
15597
  if ((result.errorCode ?? 0) > 0) {
15460
15598
  if (result.errorMessage) {
15461
15599
  this.apiLog("terminate", `Terminate failed with error: ${result.errorMessage}`, LogLevelEnum.ERROR);
@@ -15495,6 +15633,9 @@ ${stackTrace}`);
15495
15633
  } catch (e) {
15496
15634
  returnValue = this.handleValueAccessException(CMIElement, e, returnValue);
15497
15635
  }
15636
+ if (this.lastErrorCode === "0") {
15637
+ this.checkUninitializedGet(CMIElement, returnValue);
15638
+ }
15498
15639
  this.processListeners(callbackName, CMIElement);
15499
15640
  }
15500
15641
  this.apiLog(callbackName, ": returned: " + returnValue, LogLevelEnum.INFO, CMIElement);
@@ -15504,6 +15645,10 @@ ${stackTrace}`);
15504
15645
  if (this.lastErrorCode === "0") {
15505
15646
  this.clearSCORMError(returnValue);
15506
15647
  }
15648
+ const rawReturn = returnValue;
15649
+ if (typeof rawReturn === "number" || typeof rawReturn === "boolean") {
15650
+ return String(rawReturn);
15651
+ }
15507
15652
  return returnValue;
15508
15653
  }
15509
15654
  /**
@@ -15547,10 +15692,12 @@ ${stackTrace}`);
15547
15692
  * Orders LMS to store all content parameters
15548
15693
  * @param {string} callbackName
15549
15694
  * @param {boolean} checkTerminated
15695
+ * @param {CommitTrigger} trigger - What initiated the commit
15550
15696
  * @return {string}
15551
15697
  */
15552
15698
  commit(callbackName) {
15553
15699
  let checkTerminated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
15700
+ let trigger = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "manual";
15554
15701
  this.clearScheduledCommit();
15555
15702
  let returnValue = global_constants.SCORM_TRUE;
15556
15703
  if (this.isNotInitialized()) {
@@ -15562,7 +15709,7 @@ ${stackTrace}`);
15562
15709
  this.throwSCORMError("api", errorCode);
15563
15710
  if (errorCode === 143) returnValue = global_constants.SCORM_FALSE;
15564
15711
  } else {
15565
- const result = this.storeData(false);
15712
+ const result = this.storeData(false, trigger);
15566
15713
  const errorCode = result.errorCode ?? 0;
15567
15714
  if (errorCode > 0) {
15568
15715
  if (result.errorMessage) {
@@ -15783,7 +15930,11 @@ ${stackTrace}`);
15783
15930
  * @return {string}
15784
15931
  */
15785
15932
  _commonSetCMIValue(methodName, scorm2004, CMIElement, value) {
15786
- 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;
15787
15938
  }
15788
15939
  /**
15789
15940
  * Gets a value from the CMI Object.
@@ -15797,6 +15948,17 @@ ${stackTrace}`);
15797
15948
  _commonGetCMIValue(methodName, scorm2004, CMIElement) {
15798
15949
  return this._cmiValueAccessService.getCMIValue(methodName, scorm2004, CMIElement);
15799
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) {}
15800
15962
  /**
15801
15963
  * Returns true if the API's current state is STATE_INITIALIZED
15802
15964
  *
@@ -15879,9 +16041,14 @@ ${stackTrace}`);
15879
16041
  * @param {string} functionName - The name of the function/event that occurred
15880
16042
  * @param {string} CMIElement - Optional CMI element involved in the event
15881
16043
  * @param {any} value - Optional value associated with the event
16044
+ * @param {CommitEventContext} context - Optional context for commit lifecycle events
15882
16045
  */
15883
- processListeners(functionName, CMIElement, value) {
15884
- 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
+ }
15885
16052
  }
15886
16053
  /**
15887
16054
  * Throws a SCORM error with the specified error number and optional message.
@@ -16003,23 +16170,97 @@ ${stackTrace}`);
16003
16170
  * @param {string} url - The URL to send the request to
16004
16171
  * @param {CommitObject | StringKeyMap | Array<any>} params - The parameters to send
16005
16172
  * @param {boolean} immediate - Whether to send the request immediately without waiting
16173
+ * @param {CommitTrigger} [trigger] - What initiated the commit
16006
16174
  * @returns {ResultObject} - The result of the request
16007
16175
  */
16008
16176
  processHttpRequest(url, params) {
16009
16177
  let immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
16010
- if (this.settings.enableOfflineSupport && this._offlineStorageService && !this._offlineStorageService.isDeviceOnline() && this._courseId) {
16011
- this.apiLog("processHttpRequest", "Device is offline, storing data locally", LogLevelEnum.INFO);
16012
- if (params && typeof params === "object" && "cmi" in params) {
16013
- return this._offlineStorageService.storeOffline(this._courseId, params);
16014
- } else {
16015
- this.apiLog("processHttpRequest", "Invalid commit data format for offline storage", LogLevelEnum.ERROR);
16016
- return {
16017
- result: global_constants.SCORM_FALSE,
16018
- errorCode: this._error_codes.GENERAL ?? 101
16019
- };
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();
16020
16262
  }
16021
16263
  }
16022
- 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));
16023
16264
  }
16024
16265
  /**
16025
16266
  * Schedules a commit operation to occur after a specified delay.
@@ -16203,6 +16444,11 @@ ${stackTrace}`);
16203
16444
  __publicField$v(this, "__invalid_range_code");
16204
16445
  __publicField$v(this, "__decimal_regex");
16205
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");
16206
16452
  __publicField$v(this, "_raw", "");
16207
16453
  __publicField$v(this, "_min", "");
16208
16454
  __publicField$v(this, "_max");
@@ -16214,6 +16460,7 @@ ${stackTrace}`);
16214
16460
  this.__invalid_range_code = params.invalidRangeCode || scorm12_errors.VALUE_OUT_OF_RANGE;
16215
16461
  this.__decimal_regex = params.decimalRegex || scorm12_regex.CMIDecimal;
16216
16462
  this.__error_class = params.errorClass;
16463
+ this.__allow_empty_string = params.allowEmptyString ?? false;
16217
16464
  }
16218
16465
  /**
16219
16466
  * Called when the API has been reset
@@ -16253,7 +16500,7 @@ ${stackTrace}`);
16253
16500
  * @param {string} raw
16254
16501
  */
16255
16502
  set raw(raw) {
16256
- 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)) {
16257
16504
  this._raw = raw;
16258
16505
  }
16259
16506
  }
@@ -16269,7 +16516,7 @@ ${stackTrace}`);
16269
16516
  * @param {string} min
16270
16517
  */
16271
16518
  set min(min) {
16272
- 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)) {
16273
16520
  this._min = min;
16274
16521
  }
16275
16522
  }
@@ -16285,7 +16532,7 @@ ${stackTrace}`);
16285
16532
  * @param {string} max
16286
16533
  */
16287
16534
  set max(max) {
16288
- 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)) {
16289
16536
  this._max = max;
16290
16537
  }
16291
16538
  }
@@ -16362,7 +16609,9 @@ ${stackTrace}`);
16362
16609
  invalidErrorCode: scorm12_errors.INVALID_SET_VALUE,
16363
16610
  invalidTypeCode: scorm12_errors.TYPE_MISMATCH,
16364
16611
  invalidRangeCode: scorm12_errors.VALUE_OUT_OF_RANGE,
16365
- 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
16366
16615
  });
16367
16616
  }
16368
16617
  /**
@@ -16730,7 +16979,9 @@ ${stackTrace}`);
16730
16979
  invalidErrorCode: scorm12_errors.INVALID_SET_VALUE,
16731
16980
  invalidTypeCode: scorm12_errors.TYPE_MISMATCH,
16732
16981
  invalidRangeCode: scorm12_errors.VALUE_OUT_OF_RANGE,
16733
- 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
16734
16985
  });
16735
16986
  }
16736
16987
  /**
@@ -18167,16 +18418,17 @@ ${stackTrace}`);
18167
18418
  * Attempts to store the data to the LMS
18168
18419
  *
18169
18420
  * @param {boolean} terminateCommit
18421
+ * @param {CommitTrigger} [trigger] - What initiated the commit
18170
18422
  * @return {ResultObject}
18171
18423
  */
18172
- storeData(terminateCommit) {
18424
+ storeData(terminateCommit, trigger) {
18173
18425
  if (terminateCommit) {
18174
18426
  const originalStatus = this.cmi.core.lesson_status;
18175
18427
  if (this.cmi.core.lesson_mode === "browse") {
18176
18428
  const startingStatus = this.startingData?.cmi?.core?.lesson_status || "";
18177
18429
  if (startingStatus === "" && originalStatus === "not attempted") {
18178
18430
  this.cmi.core.lesson_status = "browsed";
18179
- return this.processCommitData(terminateCommit);
18431
+ return this.processCommitData(terminateCommit, trigger);
18180
18432
  }
18181
18433
  }
18182
18434
  if (!this.cmi.core.lesson_status || !this.statusSetByModule && this.cmi.core.lesson_status === "not attempted") {
@@ -18205,12 +18457,12 @@ ${stackTrace}`);
18205
18457
  }
18206
18458
  }
18207
18459
  }
18208
- return this.processCommitData(terminateCommit);
18460
+ return this.processCommitData(terminateCommit, trigger);
18209
18461
  }
18210
- processCommitData(terminateCommit) {
18462
+ processCommitData(terminateCommit, trigger) {
18211
18463
  const commitObject = this.getCommitObject(terminateCommit);
18212
18464
  if (typeof this.settings.lmsCommitUrl === "string") {
18213
- return this.processHttpRequest(this.settings.lmsCommitUrl, commitObject, terminateCommit);
18465
+ return this.processHttpRequest(this.settings.lmsCommitUrl, commitObject, terminateCommit, trigger);
18214
18466
  } else {
18215
18467
  return {
18216
18468
  result: global_constants.SCORM_TRUE,
@@ -23459,6 +23711,10 @@ ${stackTrace}`);
23459
23711
  value
23460
23712
  }) : obj[key] = value;
23461
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
+ }
23462
23718
  class Scorm2004API extends BaseAPI {
23463
23719
  /**
23464
23720
  * Constructor for SCORM 2004 API
@@ -23958,6 +24214,35 @@ ${stackTrace}`);
23958
24214
  getCMIValue(CMIElement) {
23959
24215
  return this._commonGetCMIValue("GetValue", true, CMIElement);
23960
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
+ }
23961
24246
  /**
23962
24247
  * Returns the message that corresponds to errorNumber.
23963
24248
  * @param {(string|number)} errorNumber
@@ -24006,9 +24291,10 @@ ${stackTrace}`);
24006
24291
  /**
24007
24292
  * Attempts to store the data to the LMS
24008
24293
  * @param {boolean} terminateCommit
24294
+ * @param {CommitTrigger} [trigger] - What initiated the commit
24009
24295
  * @return {ResultObject}
24010
24296
  */
24011
- storeData(terminateCommit) {
24297
+ storeData(terminateCommit, trigger) {
24012
24298
  if (terminateCommit) {
24013
24299
  if (this.cmi.mode === "normal") {
24014
24300
  if (this.cmi.credit === "credit") {
@@ -24049,7 +24335,7 @@ ${stackTrace}`);
24049
24335
  }
24050
24336
  this._globalObjectiveManager.syncCmiToSequencingActivity(completionStatusEnum, successStatusEnum, scoreObject);
24051
24337
  if (typeof this.settings.lmsCommitUrl === "string") {
24052
- const result = this.processHttpRequest(this.settings.lmsCommitUrl, commitObject, terminateCommit);
24338
+ const result = this.processHttpRequest(this.settings.lmsCommitUrl, commitObject, terminateCommit, trigger);
24053
24339
  if (navRequest && result.navRequest !== void 0 && result.navRequest !== "" && typeof result.navRequest === "string") {
24054
24340
  const parsed = parseNavigationRequest(result.navRequest);
24055
24341
  if (!parsed.valid) {