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
package/dist/scorm2004.js CHANGED
@@ -198,6 +198,15 @@ this.Scorm2004API = (function () {
198
198
  };
199
199
  }
200
200
 
201
+ const appendQueryParam = (url, name, value) => {
202
+ const fragmentIndex = url.indexOf("#");
203
+ const baseUrl = fragmentIndex === -1 ? url : url.slice(0, fragmentIndex);
204
+ const fragment = fragmentIndex === -1 ? "" : url.slice(fragmentIndex);
205
+ const separator = baseUrl.includes("?") ? baseUrl.endsWith("?") || baseUrl.endsWith("&") ? "" : "&" : "?";
206
+ const queryParam = `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}`;
207
+ return `${baseUrl}${separator}${queryParam}${fragment}`;
208
+ };
209
+
201
210
  var __defProp$15 = Object.defineProperty;
202
211
  var __defNormalProp$15 = (obj, key, value) => key in obj ? __defProp$15(obj, key, {
203
212
  enumerable: true,
@@ -570,7 +579,10 @@ this.Scorm2004API = (function () {
570
579
  INVALID_SET_VALUE: 402,
571
580
  READ_ONLY_ELEMENT: 403,
572
581
  TYPE_MISMATCH: 405,
573
- VALUE_OUT_OF_RANGE: 407};
582
+ // SCORM 1.2 has no out-of-range error code; an out-of-range value is reported
583
+ // as 405 (incorrect data type), per the SCORM 1.2 RTE error table and the ADL
584
+ // 1.2 CTS (DataModelValidator.checkScoreDecimal failure -> CMICategory 405).
585
+ VALUE_OUT_OF_RANGE: 405};
574
586
  const scorm2004_errors = {
575
587
  ...global_errors,
576
588
  INITIALIZATION_FAILED: 102,
@@ -725,6 +737,7 @@ this.Scorm2004API = (function () {
725
737
  xhrWithCredentials: false,
726
738
  fetchMode: "cors",
727
739
  asyncModeBeaconBehavior: "never",
740
+ includeCommitSequence: false,
728
741
  responseHandler: async function (response) {
729
742
  if (typeof response !== "undefined") {
730
743
  let httpResult = null;
@@ -1138,7 +1151,7 @@ this.Scorm2004API = (function () {
1138
1151
  wrapper() {
1139
1152
  if (!this._cancelled) {
1140
1153
  if (this._API.isInitialized()) {
1141
- (async () => await this._API.commit(this._callback))();
1154
+ (async () => await this._API.commit(this._callback, false, "autocommit"))();
1142
1155
  }
1143
1156
  }
1144
1157
  }
@@ -4611,6 +4624,7 @@ this.Scorm2004API = (function () {
4611
4624
  * @param {ErrorCode} error_codes - The error codes object
4612
4625
  */
4613
4626
  constructor(settings, error_codes) {
4627
+ __publicField$Q(this, "reportsRequestCompletion", true);
4614
4628
  __publicField$Q(this, "settings");
4615
4629
  __publicField$Q(this, "error_codes");
4616
4630
  this.settings = settings;
@@ -4629,13 +4643,17 @@ this.Scorm2004API = (function () {
4629
4643
  * @param {boolean} immediate - Whether to send the request immediately without waiting
4630
4644
  * @param {Function} apiLog - Function to log API messages with appropriate levels
4631
4645
  * @param {Function} processListeners - Function to trigger event listeners for commit events
4646
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
4647
+ * @param {Function} onRequestComplete - Callback invoked after the background request settles
4632
4648
  * @return {ResultObject} - Immediate optimistic success result
4633
4649
  */
4634
4650
  processHttpRequest(url, params) {
4635
4651
  let immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
4636
4652
  let apiLog = arguments.length > 3 ? arguments[3] : undefined;
4637
4653
  let processListeners = arguments.length > 4 ? arguments[4] : undefined;
4638
- this._performAsyncRequest(url, params, immediate, apiLog, processListeners);
4654
+ let metadata = arguments.length > 5 ? arguments[5] : undefined;
4655
+ let onRequestComplete = arguments.length > 6 ? arguments[6] : undefined;
4656
+ this._performAsyncRequest(url, params, immediate, apiLog, processListeners, metadata, onRequestComplete);
4639
4657
  return {
4640
4658
  result: global_constants.SCORM_TRUE,
4641
4659
  errorCode: 0
@@ -4648,11 +4666,14 @@ this.Scorm2004API = (function () {
4648
4666
  * @param {boolean} immediate - Whether this is an immediate request
4649
4667
  * @param apiLog - Function to log API messages
4650
4668
  * @param {Function} processListeners - Function to process event listeners
4669
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
4670
+ * @param {Function} onRequestComplete - Callback invoked after the request settles
4651
4671
  * @private
4652
4672
  */
4653
- async _performAsyncRequest(url, params, immediate, apiLog, processListeners) {
4673
+ async _performAsyncRequest(url, params, immediate, apiLog, processListeners, metadata, onRequestComplete) {
4654
4674
  try {
4655
- const processedParams = this.settings.requestHandler(params);
4675
+ const handledParams = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
4676
+ const processedParams = handledParams;
4656
4677
  let response;
4657
4678
  if (immediate && this.settings.asyncModeBeaconBehavior !== "never") {
4658
4679
  response = await this.performBeacon(url, processedParams);
@@ -4668,7 +4689,9 @@ this.Scorm2004API = (function () {
4668
4689
  } catch (e) {
4669
4690
  const message = e instanceof Error ? e.message : String(e);
4670
4691
  apiLog("processHttpRequest", `Async request failed: ${message}`, LogLevelEnum.ERROR);
4671
- processListeners("CommitError");
4692
+ processListeners("CommitError", void 0, this.error_codes.GENERAL_COMMIT_FAILURE || 391);
4693
+ } finally {
4694
+ onRequestComplete?.();
4672
4695
  }
4673
4696
  }
4674
4697
  /**
@@ -4830,10 +4853,13 @@ this.Scorm2004API = (function () {
4830
4853
  }
4831
4854
  /**
4832
4855
  * Gets the appropriate error code for undefined data model elements.
4833
- * SCORM 2004 uses UNDEFINED_DATA_MODEL, SCORM 1.2 uses GENERAL.
4856
+ * Both SCORM 2004 and SCORM 1.2 use UNDEFINED_DATA_MODEL (401): an
4857
+ * unrecognized element is "Not implemented", not a general exception. SCORM
4858
+ * 1.2 previously returned GENERAL (101) here, which is non-conformant — the
4859
+ * ADL 1.2 CTS and the SCORM 1.2 RTE spec expect 401 for an unknown element.
4834
4860
  */
4835
- getUndefinedDataModelErrorCode(scorm2004) {
4836
- return scorm2004 ? getErrorCode(this.context.errorCodes, "UNDEFINED_DATA_MODEL") : getErrorCode(this.context.errorCodes, "GENERAL");
4861
+ getUndefinedDataModelErrorCode() {
4862
+ return getErrorCode(this.context.errorCodes, "UNDEFINED_DATA_MODEL");
4837
4863
  }
4838
4864
  /**
4839
4865
  * Sets a value on a CMI element path
@@ -4857,7 +4883,7 @@ this.Scorm2004API = (function () {
4857
4883
  let returnValue = global_constants.SCORM_FALSE;
4858
4884
  let foundFirstIndex = false;
4859
4885
  const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
4860
- const invalidErrorCode = this.getUndefinedDataModelErrorCode(scorm2004);
4886
+ const invalidErrorCode = this.getUndefinedDataModelErrorCode();
4861
4887
  for (let idx = 0; idx < structure.length; idx++) {
4862
4888
  const attribute = structure[idx];
4863
4889
  if (idx === structure.length - 1) {
@@ -4897,12 +4923,13 @@ this.Scorm2004API = (function () {
4897
4923
  this.context.throwSCORMError(CMIElement, getErrorCode(this.context.errorCodes, "GENERAL_GET_FAILURE"), "The _version keyword was used incorrectly");
4898
4924
  return "";
4899
4925
  }
4926
+ this.context.setLastErrorCode("0");
4900
4927
  const structure = CMIElement.split(".");
4901
4928
  let refObject = this.context.getDataModel();
4902
4929
  let attribute = null;
4903
4930
  const uninitializedErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) has not been initialized.`;
4904
4931
  const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
4905
- const invalidErrorCode = this.getUndefinedDataModelErrorCode(scorm2004);
4932
+ const invalidErrorCode = this.getUndefinedDataModelErrorCode();
4906
4933
  for (let idx = 0; idx < structure.length; idx++) {
4907
4934
  attribute = structure[idx];
4908
4935
  const validationResult = this.validateGetAttribute(refObject, attribute, CMIElement, scorm2004, invalidErrorCode, invalidErrorMessage, idx === structure.length - 1);
@@ -5628,8 +5655,9 @@ ${stackTrace}`);
5628
5655
  * @param {string} functionName - The name of the function that triggered the event
5629
5656
  * @param {string} CMIElement - The CMI element that was affected
5630
5657
  * @param {any} value - The value that was set
5658
+ * @param {CommitEventContext} context - Optional context for commit lifecycle events
5631
5659
  */
5632
- processListeners(functionName, CMIElement, value) {
5660
+ processListeners(functionName, CMIElement, value, context) {
5633
5661
  this.apiLog(functionName, value, LogLevelEnum.INFO, CMIElement);
5634
5662
  const listeners = this.listenerMap.get(functionName);
5635
5663
  if (!listeners) return;
@@ -5649,9 +5677,17 @@ ${stackTrace}`);
5649
5677
  if (functionName.startsWith("Sequence")) {
5650
5678
  listener.callback(value);
5651
5679
  } else if (functionName === "CommitError") {
5652
- listener.callback(value);
5680
+ if (context !== void 0) {
5681
+ listener.callback(value, context);
5682
+ } else {
5683
+ listener.callback(value);
5684
+ }
5653
5685
  } else if (functionName === "CommitSuccess") {
5654
- listener.callback();
5686
+ if (context !== void 0) {
5687
+ listener.callback(context);
5688
+ } else {
5689
+ listener.callback();
5690
+ }
5655
5691
  } else {
5656
5692
  listener.callback(CMIElement, value);
5657
5693
  }
@@ -5760,16 +5796,23 @@ ${stackTrace}`);
5760
5796
  * Store commit data offline
5761
5797
  * @param {string} courseId - Identifier for the course
5762
5798
  * @param {CommitObject} commitData - The data to store offline
5799
+ * @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
5763
5800
  * @returns {ResultObject} - Result of the storage operation
5764
5801
  */
5765
- storeOffline(courseId, commitData) {
5802
+ storeOffline(courseId, commitData, metadata) {
5766
5803
  try {
5767
5804
  const queueItem = {
5768
5805
  id: `${courseId}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
5769
5806
  courseId,
5770
5807
  timestamp: Date.now(),
5771
5808
  data: commitData,
5772
- syncAttempts: 0
5809
+ syncAttempts: 0,
5810
+ ...(metadata?.isTerminateCommit !== void 0 ? {
5811
+ isTerminateCommit: metadata.isTerminateCommit
5812
+ } : {}),
5813
+ ...(metadata?.sequence !== void 0 ? {
5814
+ sequence: metadata.sequence
5815
+ } : {})
5773
5816
  };
5774
5817
  const currentQueue = this.getFromStorage(this.syncQueue) || [];
5775
5818
  currentQueue.push(queueItem);
@@ -5828,7 +5871,14 @@ ${stackTrace}`);
5828
5871
  continue;
5829
5872
  }
5830
5873
  try {
5831
- const syncResult = await this.sendDataToLMS(item.data);
5874
+ const syncResult = await this.sendDataToLMS(item.data, {
5875
+ ...(item.isTerminateCommit !== void 0 ? {
5876
+ isTerminateCommit: item.isTerminateCommit
5877
+ } : {}),
5878
+ ...(item.sequence !== void 0 ? {
5879
+ sequence: item.sequence
5880
+ } : {})
5881
+ });
5832
5882
  if (syncResult.result === true || syncResult.result === global_constants.SCORM_TRUE) {
5833
5883
  this.apiLog("OfflineStorageService", `Successfully synced item ${item.id}`, LogLevelEnum.INFO);
5834
5884
  } else {
@@ -5855,17 +5905,27 @@ ${stackTrace}`);
5855
5905
  /**
5856
5906
  * Send data to the LMS when online
5857
5907
  * @param {CommitObject} data - The data to send to the LMS
5908
+ * @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
5858
5909
  * @returns {Promise<ResultObject>} - Result of the sync operation
5859
5910
  */
5860
- async sendDataToLMS(data) {
5861
- if (!this.settings.lmsCommitUrl) {
5911
+ async sendDataToLMS(data, metadata) {
5912
+ const configuredCommitUrl = this.settings.lmsCommitUrl;
5913
+ if (!configuredCommitUrl) {
5862
5914
  return {
5863
5915
  result: global_constants.SCORM_FALSE,
5864
5916
  errorCode: this.error_codes.GENERAL || 101
5865
5917
  };
5866
5918
  }
5867
5919
  try {
5868
- const processedData = this.settings.requestHandler(data);
5920
+ const lmsCommitUrl = String(configuredCommitUrl);
5921
+ const processedData = this.settings.requestHandler(data, {
5922
+ isTerminateCommit: metadata?.isTerminateCommit ?? false,
5923
+ trigger: "offline-replay",
5924
+ ...(metadata?.sequence !== void 0 ? {
5925
+ sequence: metadata.sequence
5926
+ } : {})
5927
+ });
5928
+ const requestUrl = metadata?.isTerminateCommit && this.settings.terminateCommitParam ? appendQueryParam(lmsCommitUrl, this.settings.terminateCommitParam, "true") : lmsCommitUrl;
5869
5929
  const init = {
5870
5930
  method: "POST",
5871
5931
  mode: this.settings.fetchMode,
@@ -5878,7 +5938,7 @@ ${stackTrace}`);
5878
5938
  if (this.settings.xhrWithCredentials) {
5879
5939
  init.credentials = "include";
5880
5940
  }
5881
- const response = await fetch(this.settings.lmsCommitUrl, init);
5941
+ const response = await fetch(requestUrl, init);
5882
5942
  const result = typeof this.settings.responseHandler === "function" ? await this.settings.responseHandler(response) : await response.json();
5883
5943
  if (response.status >= 200 && response.status <= 299 && (result.result === true || result.result === global_constants.SCORM_TRUE)) {
5884
5944
  if (!Object.hasOwnProperty.call(result, "errorCode")) {
@@ -14775,6 +14835,8 @@ ${stackTrace}`);
14775
14835
  * @param {boolean} immediate - Whether this is a termination commit (use sendBeacon)
14776
14836
  * @param {Function} _apiLog - Function to log API messages (unused in synchronous mode - errors returned directly)
14777
14837
  * @param {Function} _processListeners - Function to trigger event listeners (unused in synchronous mode - no async events)
14838
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
14839
+ * @param {Function} _onRequestComplete - Completion callback (unused because requests settle before return)
14778
14840
  * @return {ResultObject} - The result of the request (synchronous)
14779
14841
  *
14780
14842
  * @remarks
@@ -14786,20 +14848,23 @@ ${stackTrace}`);
14786
14848
  */
14787
14849
  processHttpRequest(url, params) {
14788
14850
  let immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
14851
+ let metadata = arguments.length > 5 ? arguments[5] : undefined;
14789
14852
  if (immediate) {
14790
- return this._handleImmediateRequest(url, params);
14853
+ return this._handleImmediateRequest(url, params, metadata);
14791
14854
  }
14792
- return this._performSyncXHR(url, params);
14855
+ return this._performSyncXHR(url, params, metadata);
14793
14856
  }
14794
14857
  /**
14795
14858
  * Handles an immediate request using sendBeacon
14796
14859
  * @param {string} url - The URL to send the request to
14797
14860
  * @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
14861
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
14798
14862
  * @return {ResultObject} - The result based on beacon success
14799
14863
  * @private
14800
14864
  */
14801
- _handleImmediateRequest(url, params) {
14802
- const requestPayload = this.settings.requestHandler(params) ?? params;
14865
+ _handleImmediateRequest(url, params, metadata) {
14866
+ const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
14867
+ const requestPayload = handledPayload ?? params;
14803
14868
  const {
14804
14869
  body
14805
14870
  } = this._prepareRequestBody(requestPayload);
@@ -14815,11 +14880,13 @@ ${stackTrace}`);
14815
14880
  * Performs a synchronous XMLHttpRequest
14816
14881
  * @param {string} url - The URL to send the request to
14817
14882
  * @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
14883
+ * @param {CommitMetadata} metadata - Metadata describing the captured commit
14818
14884
  * @return {ResultObject} - The result of the request
14819
14885
  * @private
14820
14886
  */
14821
- _performSyncXHR(url, params) {
14822
- const requestPayload = this.settings.requestHandler(params) ?? params;
14887
+ _performSyncXHR(url, params, metadata) {
14888
+ const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
14889
+ const requestPayload = handledPayload ?? params;
14823
14890
  const {
14824
14891
  body,
14825
14892
  contentType
@@ -14892,9 +14959,16 @@ ${stackTrace}`);
14892
14959
  * @param {number} invalidTypeCode - The error code for invalid type
14893
14960
  * @param {number} invalidRangeCode - The error code for invalid range
14894
14961
  * @param {typeof BaseScormValidationError} errorClass - The error class to use for validation errors
14962
+ * @param {boolean} allowEmptyString - When true, an empty string is accepted (clears the value).
14963
+ * SCORM 1.2 score elements may be blank per the ADL 1.2 CTS
14964
+ * (DataModelValidator.checkScoreDecimal treats blank as valid).
14895
14965
  * @return {boolean} - True if validation passes, throws an error otherwise
14896
14966
  */
14897
14967
  validateScore(CMIElement, value, decimalRegex, scoreRange, invalidTypeCode, invalidRangeCode, errorClass) {
14968
+ let allowEmptyString = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : false;
14969
+ if (allowEmptyString && value === "") {
14970
+ return true;
14971
+ }
14898
14972
  return checkValidFormat(CMIElement, value, decimalRegex, invalidTypeCode, errorClass) && (!scoreRange || checkValidRange(CMIElement, value, scoreRange, invalidRangeCode, errorClass));
14899
14973
  }
14900
14974
  /**
@@ -14990,6 +15064,21 @@ ${stackTrace}`);
14990
15064
  __publicField$o(this, "_offlineStorageService");
14991
15065
  __publicField$o(this, "_cmiValueAccessService");
14992
15066
  __publicField$o(this, "_courseId", "");
15067
+ __publicField$o(this, "_pendingCommitCount", 0);
15068
+ /**
15069
+ * Monotonic sequence for commits captured by this API instance. It is
15070
+ * intentionally not reset by reset().
15071
+ */
15072
+ __publicField$o(this, "_commitSequence", 0);
15073
+ __publicField$o(this, "_commitSettleWaiters", []);
15074
+ /**
15075
+ * Canonical paths of every CMI element that has been explicitly assigned a
15076
+ * value via SetValue / loadFromJSON (both funnel through _commonSetCMIValue).
15077
+ * Used by standards that must tell "implemented but never set" apart from a
15078
+ * legitimately empty value when answering GetValue (SCORM 2004 error 403).
15079
+ * Cleared on reset so a fresh SCO attempt starts with nothing "set".
15080
+ */
15081
+ __publicField$o(this, "_setCMIElements", /* @__PURE__ */new Set());
14993
15082
  __publicField$o(this, "startingData");
14994
15083
  __publicField$o(this, "currentState");
14995
15084
  if (new.target === BaseAPI) {
@@ -15136,6 +15225,7 @@ ${stackTrace}`);
15136
15225
  this.lastErrorCode = "0";
15137
15226
  this._eventService.reset();
15138
15227
  this.startingData = {};
15228
+ this._setCMIElements.clear();
15139
15229
  if (this._offlineStorageService) {
15140
15230
  this._offlineStorageService.updateSettings(this.settings);
15141
15231
  if (settings?.courseId) {
@@ -15219,6 +15309,54 @@ ${stackTrace}`);
15219
15309
  this._loggingService?.setLogHandler(settings.onLogMessage);
15220
15310
  }
15221
15311
  }
15312
+ /**
15313
+ * Gets the number of captured commit requests that have not yet settled.
15314
+ *
15315
+ * @return {number} The number of in-flight commits
15316
+ */
15317
+ get pendingCommitCount() {
15318
+ return this._pendingCommitCount;
15319
+ }
15320
+ /**
15321
+ * Resolves when all currently in-flight commits have settled. A timeout is
15322
+ * best-effort: the promise resolves when it elapses even if commits remain,
15323
+ * and callers can inspect pendingCommitCount afterward to detect that case.
15324
+ *
15325
+ * @param {Object} [options] - Settle options
15326
+ * @param {number} [options.timeoutMs] - Maximum time to wait in milliseconds
15327
+ * @return {Promise<void>} A promise that resolves after the drain or timeout
15328
+ */
15329
+ whenCommitsSettled(options) {
15330
+ if (this._pendingCommitCount === 0) {
15331
+ return Promise.resolve();
15332
+ }
15333
+ return new Promise(resolve => {
15334
+ const waiter = {
15335
+ resolve
15336
+ };
15337
+ if (options?.timeoutMs !== void 0) {
15338
+ waiter.timeoutId = setTimeout(() => {
15339
+ const waiterIndex = this._commitSettleWaiters.indexOf(waiter);
15340
+ if (waiterIndex === -1) {
15341
+ return;
15342
+ }
15343
+ this._commitSettleWaiters.splice(waiterIndex, 1);
15344
+ resolve();
15345
+ }, options.timeoutMs);
15346
+ }
15347
+ this._commitSettleWaiters.push(waiter);
15348
+ });
15349
+ }
15350
+ /** Resolve and clear every waiter after the pending count reaches zero. */
15351
+ _flushCommitSettleWaiters() {
15352
+ const waiters = this._commitSettleWaiters.splice(0);
15353
+ for (const waiter of waiters) {
15354
+ if (waiter.timeoutId !== void 0) {
15355
+ clearTimeout(waiter.timeoutId);
15356
+ }
15357
+ waiter.resolve();
15358
+ }
15359
+ }
15222
15360
  /**
15223
15361
  * Terminates the current run of the API
15224
15362
  * @param {string} callbackName
@@ -15239,7 +15377,7 @@ ${stackTrace}`);
15239
15377
  } else {
15240
15378
  stateCheckPassed = true;
15241
15379
  this.processListeners("BeforeTerminate");
15242
- const result = this.storeData(true);
15380
+ const result = this.storeData(true, "terminate");
15243
15381
  if ((result.errorCode ?? 0) > 0) {
15244
15382
  if (result.errorMessage) {
15245
15383
  this.apiLog("terminate", `Terminate failed with error: ${result.errorMessage}`, LogLevelEnum.ERROR);
@@ -15279,6 +15417,9 @@ ${stackTrace}`);
15279
15417
  } catch (e) {
15280
15418
  returnValue = this.handleValueAccessException(CMIElement, e, returnValue);
15281
15419
  }
15420
+ if (this.lastErrorCode === "0") {
15421
+ this.checkUninitializedGet(CMIElement, returnValue);
15422
+ }
15282
15423
  this.processListeners(callbackName, CMIElement);
15283
15424
  }
15284
15425
  this.apiLog(callbackName, ": returned: " + returnValue, LogLevelEnum.INFO, CMIElement);
@@ -15288,6 +15429,10 @@ ${stackTrace}`);
15288
15429
  if (this.lastErrorCode === "0") {
15289
15430
  this.clearSCORMError(returnValue);
15290
15431
  }
15432
+ const rawReturn = returnValue;
15433
+ if (typeof rawReturn === "number" || typeof rawReturn === "boolean") {
15434
+ return String(rawReturn);
15435
+ }
15291
15436
  return returnValue;
15292
15437
  }
15293
15438
  /**
@@ -15331,10 +15476,12 @@ ${stackTrace}`);
15331
15476
  * Orders LMS to store all content parameters
15332
15477
  * @param {string} callbackName
15333
15478
  * @param {boolean} checkTerminated
15479
+ * @param {CommitTrigger} trigger - What initiated the commit
15334
15480
  * @return {string}
15335
15481
  */
15336
15482
  commit(callbackName) {
15337
15483
  let checkTerminated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
15484
+ let trigger = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "manual";
15338
15485
  this.clearScheduledCommit();
15339
15486
  let returnValue = global_constants.SCORM_TRUE;
15340
15487
  if (this.isNotInitialized()) {
@@ -15346,7 +15493,7 @@ ${stackTrace}`);
15346
15493
  this.throwSCORMError("api", errorCode);
15347
15494
  if (errorCode === 143) returnValue = global_constants.SCORM_FALSE;
15348
15495
  } else {
15349
- const result = this.storeData(false);
15496
+ const result = this.storeData(false, trigger);
15350
15497
  const errorCode = result.errorCode ?? 0;
15351
15498
  if (errorCode > 0) {
15352
15499
  if (result.errorMessage) {
@@ -15567,7 +15714,11 @@ ${stackTrace}`);
15567
15714
  * @return {string}
15568
15715
  */
15569
15716
  _commonSetCMIValue(methodName, scorm2004, CMIElement, value) {
15570
- return this._cmiValueAccessService.setCMIValue(methodName, scorm2004, CMIElement, value);
15717
+ const result = this._cmiValueAccessService.setCMIValue(methodName, scorm2004, CMIElement, value);
15718
+ if (result === global_constants.SCORM_TRUE) {
15719
+ this._setCMIElements.add(CMIElement);
15720
+ }
15721
+ return result;
15571
15722
  }
15572
15723
  /**
15573
15724
  * Gets a value from the CMI Object.
@@ -15581,6 +15732,17 @@ ${stackTrace}`);
15581
15732
  _commonGetCMIValue(methodName, scorm2004, CMIElement) {
15582
15733
  return this._cmiValueAccessService.getCMIValue(methodName, scorm2004, CMIElement);
15583
15734
  }
15735
+ /**
15736
+ * Hook invoked by getValue after a successful resolution. Standards that must
15737
+ * distinguish "implemented but never set, no default value" from a
15738
+ * legitimately empty value override this to raise VALUE_NOT_INITIALIZED.
15739
+ * Default is a no-op, so SCORM 1.2 / AICC keep returning "" with code 0.
15740
+ *
15741
+ * @param {string} _CMIElement - the element that was read
15742
+ * @param {any} _returnValue - the value getCMIValue resolved
15743
+ * @protected
15744
+ */
15745
+ checkUninitializedGet(_CMIElement, _returnValue) {}
15584
15746
  /**
15585
15747
  * Returns true if the API's current state is STATE_INITIALIZED
15586
15748
  *
@@ -15663,9 +15825,14 @@ ${stackTrace}`);
15663
15825
  * @param {string} functionName - The name of the function/event that occurred
15664
15826
  * @param {string} CMIElement - Optional CMI element involved in the event
15665
15827
  * @param {any} value - Optional value associated with the event
15828
+ * @param {CommitEventContext} context - Optional context for commit lifecycle events
15666
15829
  */
15667
- processListeners(functionName, CMIElement, value) {
15668
- this._eventService.processListeners(functionName, CMIElement, value);
15830
+ processListeners(functionName, CMIElement, value, context) {
15831
+ if (context !== void 0) {
15832
+ this._eventService.processListeners(functionName, CMIElement, value, context);
15833
+ } else {
15834
+ this._eventService.processListeners(functionName, CMIElement, value);
15835
+ }
15669
15836
  }
15670
15837
  /**
15671
15838
  * Throws a SCORM error with the specified error number and optional message.
@@ -15787,23 +15954,97 @@ ${stackTrace}`);
15787
15954
  * @param {string} url - The URL to send the request to
15788
15955
  * @param {CommitObject | StringKeyMap | Array<any>} params - The parameters to send
15789
15956
  * @param {boolean} immediate - Whether to send the request immediately without waiting
15957
+ * @param {CommitTrigger} [trigger] - What initiated the commit
15790
15958
  * @returns {ResultObject} - The result of the request
15791
15959
  */
15792
15960
  processHttpRequest(url, params) {
15793
15961
  let immediate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
15794
- if (this.settings.enableOfflineSupport && this._offlineStorageService && !this._offlineStorageService.isDeviceOnline() && this._courseId) {
15795
- this.apiLog("processHttpRequest", "Device is offline, storing data locally", LogLevelEnum.INFO);
15796
- if (params && typeof params === "object" && "cmi" in params) {
15797
- return this._offlineStorageService.storeOffline(this._courseId, params);
15798
- } else {
15799
- this.apiLog("processHttpRequest", "Invalid commit data format for offline storage", LogLevelEnum.ERROR);
15800
- return {
15801
- result: global_constants.SCORM_FALSE,
15802
- errorCode: this._error_codes.GENERAL ?? 101
15803
- };
15962
+ let trigger = arguments.length > 3 ? arguments[3] : undefined;
15963
+ const sequence = ++this._commitSequence;
15964
+ this._pendingCommitCount += 1;
15965
+ let settled = false;
15966
+ let completionDeferred = false;
15967
+ const settle = () => {
15968
+ if (settled) {
15969
+ return;
15970
+ }
15971
+ settled = true;
15972
+ this._pendingCommitCount -= 1;
15973
+ if (this._pendingCommitCount === 0) {
15974
+ this._flushCommitSettleWaiters();
15975
+ }
15976
+ };
15977
+ try {
15978
+ const resolvedTrigger = trigger ?? (immediate ? "terminate" : "manual");
15979
+ let finalParams = params;
15980
+ if (immediate && this.settings.terminateCommitPayloadField) {
15981
+ const field = this.settings.terminateCommitPayloadField;
15982
+ if (Array.isArray(finalParams)) {
15983
+ finalParams = [...finalParams, `${encodeURIComponent(field)}=true`];
15984
+ } else if (finalParams && typeof finalParams === "object") {
15985
+ finalParams = {
15986
+ ...finalParams,
15987
+ [field]: true
15988
+ };
15989
+ }
15990
+ }
15991
+ if (this.settings.includeCommitSequence === true) {
15992
+ if (Array.isArray(finalParams)) {
15993
+ finalParams = [...finalParams, `commitSequence=${sequence}`];
15994
+ } else if (finalParams && typeof finalParams === "object") {
15995
+ finalParams = {
15996
+ ...finalParams,
15997
+ commitSequence: sequence
15998
+ };
15999
+ }
16000
+ }
16001
+ const finalUrl = immediate && this.settings.terminateCommitParam ? appendQueryParam(url, this.settings.terminateCommitParam, "true") : url;
16002
+ const metadata = {
16003
+ isTerminateCommit: immediate,
16004
+ trigger: resolvedTrigger,
16005
+ sequence
16006
+ };
16007
+ const context = {
16008
+ url: finalUrl,
16009
+ trigger: resolvedTrigger,
16010
+ isTerminateCommit: immediate,
16011
+ sequence
16012
+ };
16013
+ if (this.settings.enableOfflineSupport && this._offlineStorageService && !this._offlineStorageService.isDeviceOnline() && this._courseId) {
16014
+ this.apiLog("processHttpRequest", "Device is offline, storing data locally", LogLevelEnum.INFO);
16015
+ if (finalParams && typeof finalParams === "object" && "cmi" in finalParams) {
16016
+ return this._offlineStorageService.storeOffline(this._courseId, finalParams, {
16017
+ isTerminateCommit: immediate,
16018
+ sequence
16019
+ });
16020
+ } else {
16021
+ this.apiLog("processHttpRequest", "Invalid commit data format for offline storage", LogLevelEnum.ERROR);
16022
+ return {
16023
+ result: global_constants.SCORM_FALSE,
16024
+ errorCode: this._error_codes.GENERAL ?? 101
16025
+ };
16026
+ }
16027
+ }
16028
+ const apiLog = (functionName, message, level, element) => this.apiLog(functionName, message, level, element);
16029
+ const processListeners = (functionName, CMIElement, value) => {
16030
+ if (functionName === "CommitSuccess" || functionName === "CommitError") {
16031
+ if (functionName === "CommitError" && typeof value === "number") {
16032
+ context.errorCode = value;
16033
+ }
16034
+ settle();
16035
+ this.processListeners(functionName, CMIElement, value, context);
16036
+ } else {
16037
+ this.processListeners(functionName, CMIElement, value);
16038
+ }
16039
+ };
16040
+ const result = this._httpService.processHttpRequest(finalUrl, finalParams, immediate, apiLog, processListeners, metadata, settle);
16041
+ completionDeferred = this._httpService.reportsRequestCompletion === true;
16042
+ return result;
16043
+ } finally {
16044
+ if (!completionDeferred) {
16045
+ settle();
15804
16046
  }
15805
16047
  }
15806
- 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));
15807
16048
  }
15808
16049
  /**
15809
16050
  * Schedules a commit operation to occur after a specified delay.
@@ -16696,6 +16937,11 @@ ${stackTrace}`);
16696
16937
  __publicField$l(this, "__invalid_range_code");
16697
16938
  __publicField$l(this, "__decimal_regex");
16698
16939
  __publicField$l(this, "__error_class");
16940
+ /**
16941
+ * When true, an empty string is a valid value (clears the element). SCORM 1.2
16942
+ * score elements may be blank per the ADL 1.2 CTS; SCORM 2004 leaves this off.
16943
+ */
16944
+ __publicField$l(this, "__allow_empty_string");
16699
16945
  __publicField$l(this, "_raw", "");
16700
16946
  __publicField$l(this, "_min", "");
16701
16947
  __publicField$l(this, "_max");
@@ -16707,6 +16953,7 @@ ${stackTrace}`);
16707
16953
  this.__invalid_range_code = params.invalidRangeCode || scorm12_errors.VALUE_OUT_OF_RANGE;
16708
16954
  this.__decimal_regex = params.decimalRegex || scorm12_regex.CMIDecimal;
16709
16955
  this.__error_class = params.errorClass;
16956
+ this.__allow_empty_string = params.allowEmptyString ?? false;
16710
16957
  }
16711
16958
  /**
16712
16959
  * Called when the API has been reset
@@ -16746,7 +16993,7 @@ ${stackTrace}`);
16746
16993
  * @param {string} raw
16747
16994
  */
16748
16995
  set raw(raw) {
16749
- if (validationService.validateScore(this._cmi_element + ".raw", raw, this.__decimal_regex, this.__score_range, this.__invalid_type_code, this.__invalid_range_code, this.__error_class)) {
16996
+ if (validationService.validateScore(this._cmi_element + ".raw", raw, this.__decimal_regex, this.__score_range, this.__invalid_type_code, this.__invalid_range_code, this.__error_class, this.__allow_empty_string)) {
16750
16997
  this._raw = raw;
16751
16998
  }
16752
16999
  }
@@ -16762,7 +17009,7 @@ ${stackTrace}`);
16762
17009
  * @param {string} min
16763
17010
  */
16764
17011
  set min(min) {
16765
- if (validationService.validateScore(this._cmi_element + ".min", min, this.__decimal_regex, this.__score_range, this.__invalid_type_code, this.__invalid_range_code, this.__error_class)) {
17012
+ if (validationService.validateScore(this._cmi_element + ".min", min, this.__decimal_regex, this.__score_range, this.__invalid_type_code, this.__invalid_range_code, this.__error_class, this.__allow_empty_string)) {
16766
17013
  this._min = min;
16767
17014
  }
16768
17015
  }
@@ -16778,7 +17025,7 @@ ${stackTrace}`);
16778
17025
  * @param {string} max
16779
17026
  */
16780
17027
  set max(max) {
16781
- if (validationService.validateScore(this._cmi_element + ".max", max, this.__decimal_regex, this.__score_range, this.__invalid_type_code, this.__invalid_range_code, this.__error_class)) {
17028
+ if (validationService.validateScore(this._cmi_element + ".max", max, this.__decimal_regex, this.__score_range, this.__invalid_type_code, this.__invalid_range_code, this.__error_class, this.__allow_empty_string)) {
16782
17029
  this._max = max;
16783
17030
  }
16784
17031
  }
@@ -21344,6 +21591,10 @@ ${stackTrace}`);
21344
21591
  value
21345
21592
  }) : obj[key] = value;
21346
21593
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
21594
+ const NO_DEFAULT_2004_ELEMENTS = /* @__PURE__ */new Set(["cmi.suspend_data", "cmi.location", "cmi.scaled_passing_score", "cmi.max_time_allowed", "cmi.completion_threshold", "cmi.progress_measure", "cmi.score.scaled", "cmi.score.raw", "cmi.score.min", "cmi.score.max", "cmi.objectives.N.score.scaled", "cmi.objectives.N.score.raw", "cmi.objectives.N.score.min", "cmi.objectives.N.score.max", "cmi.objectives.N.progress_measure", "cmi.objectives.N.description", "cmi.interactions.N.weighting", "cmi.interactions.N.type", "cmi.interactions.N.timestamp", "cmi.interactions.N.result", "cmi.interactions.N.latency", "cmi.interactions.N.learner_response", "cmi.interactions.N.description", "cmi.comments_from_learner.N.timestamp"]);
21595
+ function normalizeCMIIndices(CMIElement) {
21596
+ return CMIElement.replace(/\.\d+(?=\.|$)/g, ".N");
21597
+ }
21347
21598
  class Scorm2004API extends BaseAPI {
21348
21599
  /**
21349
21600
  * Constructor for SCORM 2004 API
@@ -21843,6 +22094,35 @@ ${stackTrace}`);
21843
22094
  getCMIValue(CMIElement) {
21844
22095
  return this._commonGetCMIValue("GetValue", true, CMIElement);
21845
22096
  }
22097
+ /**
22098
+ * Raises 403 (VALUE_NOT_INITIALIZED) when an implemented, no-default element is
22099
+ * read before it has ever been set, per IEEE 1484.11.2 / SCORM 2004 RTE.
22100
+ *
22101
+ * Invoked by BaseAPI.getValue after an otherwise-successful resolution, so it
22102
+ * only sees values the data model already returned cleanly. The decision is
22103
+ * deliberately gated to this public boundary (never the getters) to keep
22104
+ * serialization/commit/rollup — which read the getters directly — unaffected.
22105
+ *
22106
+ * A 403 is raised only when all hold:
22107
+ * - the resolved value is "" — any non-empty value is by definition set,
22108
+ * including values written by internal sequencing/activity-tree paths that
22109
+ * bypass _commonSetCMIValue (those only ever write non-empty values);
22110
+ * - the element was never set — _setCMIElements records every successful
22111
+ * SetValue, loadFromJSON, and global-objective restore (all route through
22112
+ * _commonSetCMIValue), so an explicit SetValue(x, "") still reads back as
22113
+ * "" with code 0; and
22114
+ * - the element has no spec-defined default ({@link NO_DEFAULT_2004_ELEMENTS}).
22115
+ *
22116
+ * @param {string} CMIElement
22117
+ * @param {*} returnValue - the value getCMIValue resolved
22118
+ * @protected
22119
+ */
22120
+ checkUninitializedGet(CMIElement, returnValue) {
22121
+ if (returnValue !== "") return;
22122
+ if (this._setCMIElements.has(CMIElement)) return;
22123
+ if (!NO_DEFAULT_2004_ELEMENTS.has(normalizeCMIIndices(CMIElement))) return;
22124
+ this.throwSCORMError(CMIElement, this._error_codes.VALUE_NOT_INITIALIZED ?? 403, `The data model element passed to GetValue (${CMIElement}) has not been initialized.`);
22125
+ }
21846
22126
  /**
21847
22127
  * Returns the message that corresponds to errorNumber.
21848
22128
  * @param {(string|number)} errorNumber
@@ -21891,9 +22171,10 @@ ${stackTrace}`);
21891
22171
  /**
21892
22172
  * Attempts to store the data to the LMS
21893
22173
  * @param {boolean} terminateCommit
22174
+ * @param {CommitTrigger} [trigger] - What initiated the commit
21894
22175
  * @return {ResultObject}
21895
22176
  */
21896
- storeData(terminateCommit) {
22177
+ storeData(terminateCommit, trigger) {
21897
22178
  if (terminateCommit) {
21898
22179
  if (this.cmi.mode === "normal") {
21899
22180
  if (this.cmi.credit === "credit") {
@@ -21934,7 +22215,7 @@ ${stackTrace}`);
21934
22215
  }
21935
22216
  this._globalObjectiveManager.syncCmiToSequencingActivity(completionStatusEnum, successStatusEnum, scoreObject);
21936
22217
  if (typeof this.settings.lmsCommitUrl === "string") {
21937
- const result = this.processHttpRequest(this.settings.lmsCommitUrl, commitObject, terminateCommit);
22218
+ const result = this.processHttpRequest(this.settings.lmsCommitUrl, commitObject, terminateCommit, trigger);
21938
22219
  if (navRequest && result.navRequest !== void 0 && result.navRequest !== "" && typeof result.navRequest === "string") {
21939
22220
  const parsed = parseNavigationRequest(result.navRequest);
21940
22221
  if (!parsed.valid) {