scorm-again 3.0.5 → 3.1.1
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.
- package/README.md +77 -1
- package/dist/cross-frame-api.js +579 -320
- package/dist/cross-frame-api.js.map +1 -1
- package/dist/cross-frame-api.min.js +1 -1
- package/dist/cross-frame-api.min.js.map +1 -1
- package/dist/cross-frame-lms.js +226 -163
- package/dist/cross-frame-lms.js.map +1 -1
- package/dist/cross-frame-lms.min.js +1 -1
- package/dist/cross-frame-lms.min.js.map +1 -1
- package/dist/esm/cross-frame-api.js.map +1 -1
- package/dist/esm/scorm-again.js +391 -86
- package/dist/esm/scorm-again.js.map +1 -1
- package/dist/esm/scorm-again.min.js +1 -1
- package/dist/esm/scorm-again.min.js.map +1 -1
- package/dist/esm/scorm12.js +323 -76
- package/dist/esm/scorm12.js.map +1 -1
- package/dist/esm/scorm12.min.js +1 -1
- package/dist/esm/scorm12.min.js.map +1 -1
- package/dist/esm/scorm2004.js +373 -72
- package/dist/esm/scorm2004.js.map +1 -1
- package/dist/esm/scorm2004.min.js +1 -1
- package/dist/esm/scorm2004.min.js.map +1 -1
- package/dist/scorm-again.js +32522 -19996
- package/dist/scorm-again.js.map +1 -1
- package/dist/scorm-again.min.js +1 -1
- package/dist/scorm-again.min.js.map +1 -1
- package/dist/scorm12.js +9130 -5031
- package/dist/scorm12.js.map +1 -1
- package/dist/scorm12.min.js +1 -1
- package/dist/scorm12.min.js.map +1 -1
- package/dist/scorm2004.js +30040 -18419
- package/dist/scorm2004.js.map +1 -1
- package/dist/scorm2004.min.js +1 -1
- package/dist/scorm2004.min.js.map +1 -1
- package/dist/types/BaseAPI.d.ts +15 -5
- package/dist/types/Scorm12API.d.ts +2 -2
- package/dist/types/Scorm2004API.d.ts +3 -2
- package/dist/types/cmi/common/score.d.ts +2 -0
- package/dist/types/interfaces/services.d.ts +8 -4
- package/dist/types/services/AsynchronousHttpService.d.ts +3 -2
- package/dist/types/services/EventService.d.ts +2 -2
- package/dist/types/services/OfflineStorageService.d.ts +6 -1
- package/dist/types/services/SynchronousHttpService.d.ts +2 -2
- package/dist/types/services/ValidationService.d.ts +1 -1
- package/dist/types/types/api_types.d.ts +22 -2
- package/dist/types/utilities/index.d.ts +1 -0
- package/dist/types/utilities/url.d.ts +2 -0
- package/package.json +27 -27
package/dist/esm/scorm2004.js
CHANGED
|
@@ -210,6 +210,15 @@ function parseNavigationRequest(navRequest) {
|
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
const appendQueryParam = (url, name, value) => {
|
|
214
|
+
const fragmentIndex = url.indexOf("#");
|
|
215
|
+
const baseUrl = fragmentIndex === -1 ? url : url.slice(0, fragmentIndex);
|
|
216
|
+
const fragment = fragmentIndex === -1 ? "" : url.slice(fragmentIndex);
|
|
217
|
+
const separator = baseUrl.includes("?") ? baseUrl.endsWith("?") || baseUrl.endsWith("&") ? "" : "&" : "?";
|
|
218
|
+
const queryParam = `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}`;
|
|
219
|
+
return `${baseUrl}${separator}${queryParam}${fragment}`;
|
|
220
|
+
};
|
|
221
|
+
|
|
213
222
|
class BaseCMI {
|
|
214
223
|
/**
|
|
215
224
|
* Flag used during JSON serialization to allow getter access without initialization checks.
|
|
@@ -583,7 +592,10 @@ const scorm12_errors = {
|
|
|
583
592
|
INVALID_SET_VALUE: 402,
|
|
584
593
|
READ_ONLY_ELEMENT: 403,
|
|
585
594
|
TYPE_MISMATCH: 405,
|
|
586
|
-
|
|
595
|
+
// SCORM 1.2 has no out-of-range error code; an out-of-range value is reported
|
|
596
|
+
// as 405 (incorrect data type), per the SCORM 1.2 RTE error table and the ADL
|
|
597
|
+
// 1.2 CTS (DataModelValidator.checkScoreDecimal failure -> CMICategory 405).
|
|
598
|
+
VALUE_OUT_OF_RANGE: 405};
|
|
587
599
|
const scorm2004_errors = {
|
|
588
600
|
...global_errors,
|
|
589
601
|
INITIALIZATION_FAILED: 102,
|
|
@@ -729,6 +741,7 @@ const DefaultSettings = {
|
|
|
729
741
|
xhrWithCredentials: false,
|
|
730
742
|
fetchMode: "cors",
|
|
731
743
|
asyncModeBeaconBehavior: "never",
|
|
744
|
+
includeCommitSequence: false,
|
|
732
745
|
responseHandler: async function(response) {
|
|
733
746
|
if (typeof response !== "undefined") {
|
|
734
747
|
let httpResult = null;
|
|
@@ -1519,7 +1532,7 @@ class ScheduledCommit {
|
|
|
1519
1532
|
wrapper() {
|
|
1520
1533
|
if (!this._cancelled) {
|
|
1521
1534
|
if (this._API.isInitialized()) {
|
|
1522
|
-
(async () => await this._API.commit(this._callback))();
|
|
1535
|
+
(async () => await this._API.commit(this._callback, false, "autocommit"))();
|
|
1523
1536
|
}
|
|
1524
1537
|
}
|
|
1525
1538
|
}
|
|
@@ -4864,6 +4877,7 @@ class ActivityDeliveryService {
|
|
|
4864
4877
|
}
|
|
4865
4878
|
|
|
4866
4879
|
class AsynchronousHttpService {
|
|
4880
|
+
reportsRequestCompletion = true;
|
|
4867
4881
|
settings;
|
|
4868
4882
|
error_codes;
|
|
4869
4883
|
/**
|
|
@@ -4888,10 +4902,20 @@ class AsynchronousHttpService {
|
|
|
4888
4902
|
* @param {boolean} immediate - Whether to send the request immediately without waiting
|
|
4889
4903
|
* @param {Function} apiLog - Function to log API messages with appropriate levels
|
|
4890
4904
|
* @param {Function} processListeners - Function to trigger event listeners for commit events
|
|
4905
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
4906
|
+
* @param {Function} onRequestComplete - Callback invoked after the background request settles
|
|
4891
4907
|
* @return {ResultObject} - Immediate optimistic success result
|
|
4892
4908
|
*/
|
|
4893
|
-
processHttpRequest(url, params, immediate = false, apiLog, processListeners) {
|
|
4894
|
-
this._performAsyncRequest(
|
|
4909
|
+
processHttpRequest(url, params, immediate = false, apiLog, processListeners, metadata, onRequestComplete) {
|
|
4910
|
+
this._performAsyncRequest(
|
|
4911
|
+
url,
|
|
4912
|
+
params,
|
|
4913
|
+
immediate,
|
|
4914
|
+
apiLog,
|
|
4915
|
+
processListeners,
|
|
4916
|
+
metadata,
|
|
4917
|
+
onRequestComplete
|
|
4918
|
+
);
|
|
4895
4919
|
return {
|
|
4896
4920
|
result: global_constants.SCORM_TRUE,
|
|
4897
4921
|
errorCode: 0
|
|
@@ -4904,11 +4928,14 @@ class AsynchronousHttpService {
|
|
|
4904
4928
|
* @param {boolean} immediate - Whether this is an immediate request
|
|
4905
4929
|
* @param apiLog - Function to log API messages
|
|
4906
4930
|
* @param {Function} processListeners - Function to process event listeners
|
|
4931
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
4932
|
+
* @param {Function} onRequestComplete - Callback invoked after the request settles
|
|
4907
4933
|
* @private
|
|
4908
4934
|
*/
|
|
4909
|
-
async _performAsyncRequest(url, params, immediate, apiLog, processListeners) {
|
|
4935
|
+
async _performAsyncRequest(url, params, immediate, apiLog, processListeners, metadata, onRequestComplete) {
|
|
4910
4936
|
try {
|
|
4911
|
-
const
|
|
4937
|
+
const handledParams = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
|
|
4938
|
+
const processedParams = handledParams;
|
|
4912
4939
|
let response;
|
|
4913
4940
|
if (immediate && this.settings.asyncModeBeaconBehavior !== "never") {
|
|
4914
4941
|
response = await this.performBeacon(url, processedParams);
|
|
@@ -4924,7 +4951,9 @@ class AsynchronousHttpService {
|
|
|
4924
4951
|
} catch (e) {
|
|
4925
4952
|
const message = e instanceof Error ? e.message : String(e);
|
|
4926
4953
|
apiLog("processHttpRequest", `Async request failed: ${message}`, LogLevelEnum.ERROR);
|
|
4927
|
-
processListeners("CommitError");
|
|
4954
|
+
processListeners("CommitError", void 0, this.error_codes.GENERAL_COMMIT_FAILURE || 391);
|
|
4955
|
+
} finally {
|
|
4956
|
+
onRequestComplete?.();
|
|
4928
4957
|
}
|
|
4929
4958
|
}
|
|
4930
4959
|
/**
|
|
@@ -5067,10 +5096,13 @@ class CMIValueAccessService {
|
|
|
5067
5096
|
}
|
|
5068
5097
|
/**
|
|
5069
5098
|
* Gets the appropriate error code for undefined data model elements.
|
|
5070
|
-
* SCORM 2004
|
|
5099
|
+
* Both SCORM 2004 and SCORM 1.2 use UNDEFINED_DATA_MODEL (401): an
|
|
5100
|
+
* unrecognized element is "Not implemented", not a general exception. SCORM
|
|
5101
|
+
* 1.2 previously returned GENERAL (101) here, which is non-conformant — the
|
|
5102
|
+
* ADL 1.2 CTS and the SCORM 1.2 RTE spec expect 401 for an unknown element.
|
|
5071
5103
|
*/
|
|
5072
|
-
getUndefinedDataModelErrorCode(
|
|
5073
|
-
return
|
|
5104
|
+
getUndefinedDataModelErrorCode() {
|
|
5105
|
+
return getErrorCode(this.context.errorCodes, "UNDEFINED_DATA_MODEL");
|
|
5074
5106
|
}
|
|
5075
5107
|
/**
|
|
5076
5108
|
* Sets a value on a CMI element path
|
|
@@ -5098,7 +5130,7 @@ class CMIValueAccessService {
|
|
|
5098
5130
|
let returnValue = global_constants.SCORM_FALSE;
|
|
5099
5131
|
let foundFirstIndex = false;
|
|
5100
5132
|
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
|
|
5101
|
-
const invalidErrorCode = this.getUndefinedDataModelErrorCode(
|
|
5133
|
+
const invalidErrorCode = this.getUndefinedDataModelErrorCode();
|
|
5102
5134
|
for (let idx = 0; idx < structure.length; idx++) {
|
|
5103
5135
|
const attribute = structure[idx];
|
|
5104
5136
|
if (idx === structure.length - 1) {
|
|
@@ -5168,12 +5200,13 @@ class CMIValueAccessService {
|
|
|
5168
5200
|
);
|
|
5169
5201
|
return "";
|
|
5170
5202
|
}
|
|
5203
|
+
this.context.setLastErrorCode("0");
|
|
5171
5204
|
const structure = CMIElement.split(".");
|
|
5172
5205
|
let refObject = this.context.getDataModel();
|
|
5173
5206
|
let attribute = null;
|
|
5174
5207
|
const uninitializedErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) has not been initialized.`;
|
|
5175
5208
|
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
|
|
5176
|
-
const invalidErrorCode = this.getUndefinedDataModelErrorCode(
|
|
5209
|
+
const invalidErrorCode = this.getUndefinedDataModelErrorCode();
|
|
5177
5210
|
for (let idx = 0; idx < structure.length; idx++) {
|
|
5178
5211
|
attribute = structure[idx];
|
|
5179
5212
|
const validationResult = this.validateGetAttribute(
|
|
@@ -5885,8 +5918,9 @@ class EventService {
|
|
|
5885
5918
|
* @param {string} functionName - The name of the function that triggered the event
|
|
5886
5919
|
* @param {string} CMIElement - The CMI element that was affected
|
|
5887
5920
|
* @param {any} value - The value that was set
|
|
5921
|
+
* @param {CommitEventContext} context - Optional context for commit lifecycle events
|
|
5888
5922
|
*/
|
|
5889
|
-
processListeners(functionName, CMIElement, value) {
|
|
5923
|
+
processListeners(functionName, CMIElement, value, context) {
|
|
5890
5924
|
this.apiLog(functionName, value, LogLevelEnum.INFO, CMIElement);
|
|
5891
5925
|
const listeners = this.listenerMap.get(functionName);
|
|
5892
5926
|
if (!listeners) return;
|
|
@@ -5911,9 +5945,17 @@ class EventService {
|
|
|
5911
5945
|
if (functionName.startsWith("Sequence")) {
|
|
5912
5946
|
listener.callback(value);
|
|
5913
5947
|
} else if (functionName === "CommitError") {
|
|
5914
|
-
|
|
5948
|
+
if (context !== void 0) {
|
|
5949
|
+
listener.callback(value, context);
|
|
5950
|
+
} else {
|
|
5951
|
+
listener.callback(value);
|
|
5952
|
+
}
|
|
5915
5953
|
} else if (functionName === "CommitSuccess") {
|
|
5916
|
-
|
|
5954
|
+
if (context !== void 0) {
|
|
5955
|
+
listener.callback(context);
|
|
5956
|
+
} else {
|
|
5957
|
+
listener.callback();
|
|
5958
|
+
}
|
|
5917
5959
|
} else {
|
|
5918
5960
|
listener.callback(CMIElement, value);
|
|
5919
5961
|
}
|
|
@@ -6047,16 +6089,19 @@ class OfflineStorageService {
|
|
|
6047
6089
|
* Store commit data offline
|
|
6048
6090
|
* @param {string} courseId - Identifier for the course
|
|
6049
6091
|
* @param {CommitObject} commitData - The data to store offline
|
|
6092
|
+
* @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
|
|
6050
6093
|
* @returns {ResultObject} - Result of the storage operation
|
|
6051
6094
|
*/
|
|
6052
|
-
storeOffline(courseId, commitData) {
|
|
6095
|
+
storeOffline(courseId, commitData, metadata) {
|
|
6053
6096
|
try {
|
|
6054
6097
|
const queueItem = {
|
|
6055
6098
|
id: `${courseId}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
|
6056
6099
|
courseId,
|
|
6057
6100
|
timestamp: Date.now(),
|
|
6058
6101
|
data: commitData,
|
|
6059
|
-
syncAttempts: 0
|
|
6102
|
+
syncAttempts: 0,
|
|
6103
|
+
...metadata?.isTerminateCommit !== void 0 ? { isTerminateCommit: metadata.isTerminateCommit } : {},
|
|
6104
|
+
...metadata?.sequence !== void 0 ? { sequence: metadata.sequence } : {}
|
|
6060
6105
|
};
|
|
6061
6106
|
const currentQueue = this.getFromStorage(this.syncQueue) || [];
|
|
6062
6107
|
currentQueue.push(queueItem);
|
|
@@ -6135,7 +6180,10 @@ class OfflineStorageService {
|
|
|
6135
6180
|
continue;
|
|
6136
6181
|
}
|
|
6137
6182
|
try {
|
|
6138
|
-
const syncResult = await this.sendDataToLMS(item.data
|
|
6183
|
+
const syncResult = await this.sendDataToLMS(item.data, {
|
|
6184
|
+
...item.isTerminateCommit !== void 0 ? { isTerminateCommit: item.isTerminateCommit } : {},
|
|
6185
|
+
...item.sequence !== void 0 ? { sequence: item.sequence } : {}
|
|
6186
|
+
});
|
|
6139
6187
|
if (syncResult.result === true || syncResult.result === global_constants.SCORM_TRUE) {
|
|
6140
6188
|
this.apiLog(
|
|
6141
6189
|
"OfflineStorageService",
|
|
@@ -6182,17 +6230,25 @@ class OfflineStorageService {
|
|
|
6182
6230
|
/**
|
|
6183
6231
|
* Send data to the LMS when online
|
|
6184
6232
|
* @param {CommitObject} data - The data to send to the LMS
|
|
6233
|
+
* @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
|
|
6185
6234
|
* @returns {Promise<ResultObject>} - Result of the sync operation
|
|
6186
6235
|
*/
|
|
6187
|
-
async sendDataToLMS(data) {
|
|
6188
|
-
|
|
6236
|
+
async sendDataToLMS(data, metadata) {
|
|
6237
|
+
const configuredCommitUrl = this.settings.lmsCommitUrl;
|
|
6238
|
+
if (!configuredCommitUrl) {
|
|
6189
6239
|
return {
|
|
6190
6240
|
result: global_constants.SCORM_FALSE,
|
|
6191
6241
|
errorCode: this.error_codes.GENERAL || 101
|
|
6192
6242
|
};
|
|
6193
6243
|
}
|
|
6194
6244
|
try {
|
|
6195
|
-
const
|
|
6245
|
+
const lmsCommitUrl = String(configuredCommitUrl);
|
|
6246
|
+
const processedData = this.settings.requestHandler(data, {
|
|
6247
|
+
isTerminateCommit: metadata?.isTerminateCommit ?? false,
|
|
6248
|
+
trigger: "offline-replay",
|
|
6249
|
+
...metadata?.sequence !== void 0 ? { sequence: metadata.sequence } : {}
|
|
6250
|
+
});
|
|
6251
|
+
const requestUrl = metadata?.isTerminateCommit && this.settings.terminateCommitParam ? appendQueryParam(lmsCommitUrl, this.settings.terminateCommitParam, "true") : lmsCommitUrl;
|
|
6196
6252
|
const init = {
|
|
6197
6253
|
method: "POST",
|
|
6198
6254
|
mode: this.settings.fetchMode,
|
|
@@ -6205,7 +6261,7 @@ class OfflineStorageService {
|
|
|
6205
6261
|
if (this.settings.xhrWithCredentials) {
|
|
6206
6262
|
init.credentials = "include";
|
|
6207
6263
|
}
|
|
6208
|
-
const response = await fetch(
|
|
6264
|
+
const response = await fetch(requestUrl, init);
|
|
6209
6265
|
const result = typeof this.settings.responseHandler === "function" ? await this.settings.responseHandler(response) : await response.json();
|
|
6210
6266
|
if (response.status >= 200 && response.status <= 299 && (result.result === true || result.result === global_constants.SCORM_TRUE)) {
|
|
6211
6267
|
if (!Object.hasOwnProperty.call(result, "errorCode")) {
|
|
@@ -15170,6 +15226,8 @@ class SynchronousHttpService {
|
|
|
15170
15226
|
* @param {boolean} immediate - Whether this is a termination commit (use sendBeacon)
|
|
15171
15227
|
* @param {Function} _apiLog - Function to log API messages (unused in synchronous mode - errors returned directly)
|
|
15172
15228
|
* @param {Function} _processListeners - Function to trigger event listeners (unused in synchronous mode - no async events)
|
|
15229
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
15230
|
+
* @param {Function} _onRequestComplete - Completion callback (unused because requests settle before return)
|
|
15173
15231
|
* @return {ResultObject} - The result of the request (synchronous)
|
|
15174
15232
|
*
|
|
15175
15233
|
* @remarks
|
|
@@ -15179,21 +15237,23 @@ class SynchronousHttpService {
|
|
|
15179
15237
|
* - No async events need to be triggered (CommitSuccess/CommitError) since results are synchronous
|
|
15180
15238
|
* - AsynchronousHttpService uses these parameters to handle background request results
|
|
15181
15239
|
*/
|
|
15182
|
-
processHttpRequest(url, params, immediate = false, _apiLog, _processListeners) {
|
|
15240
|
+
processHttpRequest(url, params, immediate = false, _apiLog, _processListeners, metadata, _onRequestComplete) {
|
|
15183
15241
|
if (immediate) {
|
|
15184
|
-
return this._handleImmediateRequest(url, params);
|
|
15242
|
+
return this._handleImmediateRequest(url, params, metadata);
|
|
15185
15243
|
}
|
|
15186
|
-
return this._performSyncXHR(url, params);
|
|
15244
|
+
return this._performSyncXHR(url, params, metadata);
|
|
15187
15245
|
}
|
|
15188
15246
|
/**
|
|
15189
15247
|
* Handles an immediate request using sendBeacon
|
|
15190
15248
|
* @param {string} url - The URL to send the request to
|
|
15191
15249
|
* @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
|
|
15250
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
15192
15251
|
* @return {ResultObject} - The result based on beacon success
|
|
15193
15252
|
* @private
|
|
15194
15253
|
*/
|
|
15195
|
-
_handleImmediateRequest(url, params) {
|
|
15196
|
-
const
|
|
15254
|
+
_handleImmediateRequest(url, params, metadata) {
|
|
15255
|
+
const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
|
|
15256
|
+
const requestPayload = handledPayload ?? params;
|
|
15197
15257
|
const { body } = this._prepareRequestBody(requestPayload);
|
|
15198
15258
|
const beaconSuccess = navigator.sendBeacon(
|
|
15199
15259
|
url,
|
|
@@ -15208,11 +15268,13 @@ class SynchronousHttpService {
|
|
|
15208
15268
|
* Performs a synchronous XMLHttpRequest
|
|
15209
15269
|
* @param {string} url - The URL to send the request to
|
|
15210
15270
|
* @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
|
|
15271
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
15211
15272
|
* @return {ResultObject} - The result of the request
|
|
15212
15273
|
* @private
|
|
15213
15274
|
*/
|
|
15214
|
-
_performSyncXHR(url, params) {
|
|
15215
|
-
const
|
|
15275
|
+
_performSyncXHR(url, params, metadata) {
|
|
15276
|
+
const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
|
|
15277
|
+
const requestPayload = handledPayload ?? params;
|
|
15216
15278
|
const { body, contentType } = this._prepareRequestBody(requestPayload);
|
|
15217
15279
|
const xhr = new XMLHttpRequest();
|
|
15218
15280
|
xhr.open("POST", url, false);
|
|
@@ -15291,9 +15353,15 @@ class ValidationService {
|
|
|
15291
15353
|
* @param {number} invalidTypeCode - The error code for invalid type
|
|
15292
15354
|
* @param {number} invalidRangeCode - The error code for invalid range
|
|
15293
15355
|
* @param {typeof BaseScormValidationError} errorClass - The error class to use for validation errors
|
|
15356
|
+
* @param {boolean} allowEmptyString - When true, an empty string is accepted (clears the value).
|
|
15357
|
+
* SCORM 1.2 score elements may be blank per the ADL 1.2 CTS
|
|
15358
|
+
* (DataModelValidator.checkScoreDecimal treats blank as valid).
|
|
15294
15359
|
* @return {boolean} - True if validation passes, throws an error otherwise
|
|
15295
15360
|
*/
|
|
15296
|
-
validateScore(CMIElement, value, decimalRegex, scoreRange, invalidTypeCode, invalidRangeCode, errorClass) {
|
|
15361
|
+
validateScore(CMIElement, value, decimalRegex, scoreRange, invalidTypeCode, invalidRangeCode, errorClass, allowEmptyString = false) {
|
|
15362
|
+
if (allowEmptyString && value === "") {
|
|
15363
|
+
return true;
|
|
15364
|
+
}
|
|
15297
15365
|
return checkValidFormat(CMIElement, value, decimalRegex, invalidTypeCode, errorClass) && (!scoreRange || checkValidRange(CMIElement, value, scoreRange, invalidRangeCode, errorClass));
|
|
15298
15366
|
}
|
|
15299
15367
|
/**
|
|
@@ -15367,6 +15435,21 @@ class BaseAPI {
|
|
|
15367
15435
|
_offlineStorageService;
|
|
15368
15436
|
_cmiValueAccessService;
|
|
15369
15437
|
_courseId = "";
|
|
15438
|
+
_pendingCommitCount = 0;
|
|
15439
|
+
/**
|
|
15440
|
+
* Monotonic sequence for commits captured by this API instance. It is
|
|
15441
|
+
* intentionally not reset by reset().
|
|
15442
|
+
*/
|
|
15443
|
+
_commitSequence = 0;
|
|
15444
|
+
_commitSettleWaiters = [];
|
|
15445
|
+
/**
|
|
15446
|
+
* Canonical paths of every CMI element that has been explicitly assigned a
|
|
15447
|
+
* value via SetValue / loadFromJSON (both funnel through _commonSetCMIValue).
|
|
15448
|
+
* Used by standards that must tell "implemented but never set" apart from a
|
|
15449
|
+
* legitimately empty value when answering GetValue (SCORM 2004 error 403).
|
|
15450
|
+
* Cleared on reset so a fresh SCO attempt starts with nothing "set".
|
|
15451
|
+
*/
|
|
15452
|
+
_setCMIElements = /* @__PURE__ */ new Set();
|
|
15370
15453
|
/**
|
|
15371
15454
|
* Constructor for Base API class. Sets some shared API fields, as well as
|
|
15372
15455
|
* sets up options for the API.
|
|
@@ -15552,6 +15635,7 @@ class BaseAPI {
|
|
|
15552
15635
|
this.lastErrorCode = "0";
|
|
15553
15636
|
this._eventService.reset();
|
|
15554
15637
|
this.startingData = {};
|
|
15638
|
+
this._setCMIElements.clear();
|
|
15555
15639
|
if (this._offlineStorageService) {
|
|
15556
15640
|
this._offlineStorageService.updateSettings(this.settings);
|
|
15557
15641
|
if (settings?.courseId) {
|
|
@@ -15636,6 +15720,52 @@ class BaseAPI {
|
|
|
15636
15720
|
this._loggingService?.setLogHandler(settings.onLogMessage);
|
|
15637
15721
|
}
|
|
15638
15722
|
}
|
|
15723
|
+
/**
|
|
15724
|
+
* Gets the number of captured commit requests that have not yet settled.
|
|
15725
|
+
*
|
|
15726
|
+
* @return {number} The number of in-flight commits
|
|
15727
|
+
*/
|
|
15728
|
+
get pendingCommitCount() {
|
|
15729
|
+
return this._pendingCommitCount;
|
|
15730
|
+
}
|
|
15731
|
+
/**
|
|
15732
|
+
* Resolves when all currently in-flight commits have settled. A timeout is
|
|
15733
|
+
* best-effort: the promise resolves when it elapses even if commits remain,
|
|
15734
|
+
* and callers can inspect pendingCommitCount afterward to detect that case.
|
|
15735
|
+
*
|
|
15736
|
+
* @param {Object} [options] - Settle options
|
|
15737
|
+
* @param {number} [options.timeoutMs] - Maximum time to wait in milliseconds
|
|
15738
|
+
* @return {Promise<void>} A promise that resolves after the drain or timeout
|
|
15739
|
+
*/
|
|
15740
|
+
whenCommitsSettled(options) {
|
|
15741
|
+
if (this._pendingCommitCount === 0) {
|
|
15742
|
+
return Promise.resolve();
|
|
15743
|
+
}
|
|
15744
|
+
return new Promise((resolve) => {
|
|
15745
|
+
const waiter = { resolve };
|
|
15746
|
+
if (options?.timeoutMs !== void 0) {
|
|
15747
|
+
waiter.timeoutId = setTimeout(() => {
|
|
15748
|
+
const waiterIndex = this._commitSettleWaiters.indexOf(waiter);
|
|
15749
|
+
if (waiterIndex === -1) {
|
|
15750
|
+
return;
|
|
15751
|
+
}
|
|
15752
|
+
this._commitSettleWaiters.splice(waiterIndex, 1);
|
|
15753
|
+
resolve();
|
|
15754
|
+
}, options.timeoutMs);
|
|
15755
|
+
}
|
|
15756
|
+
this._commitSettleWaiters.push(waiter);
|
|
15757
|
+
});
|
|
15758
|
+
}
|
|
15759
|
+
/** Resolve and clear every waiter after the pending count reaches zero. */
|
|
15760
|
+
_flushCommitSettleWaiters() {
|
|
15761
|
+
const waiters = this._commitSettleWaiters.splice(0);
|
|
15762
|
+
for (const waiter of waiters) {
|
|
15763
|
+
if (waiter.timeoutId !== void 0) {
|
|
15764
|
+
clearTimeout(waiter.timeoutId);
|
|
15765
|
+
}
|
|
15766
|
+
waiter.resolve();
|
|
15767
|
+
}
|
|
15768
|
+
}
|
|
15639
15769
|
/**
|
|
15640
15770
|
* Terminates the current run of the API
|
|
15641
15771
|
* @param {string} callbackName
|
|
@@ -15656,7 +15786,7 @@ class BaseAPI {
|
|
|
15656
15786
|
} else {
|
|
15657
15787
|
stateCheckPassed = true;
|
|
15658
15788
|
this.processListeners("BeforeTerminate");
|
|
15659
|
-
const result = this.storeData(true);
|
|
15789
|
+
const result = this.storeData(true, "terminate");
|
|
15660
15790
|
if ((result.errorCode ?? 0) > 0) {
|
|
15661
15791
|
if (result.errorMessage) {
|
|
15662
15792
|
this.apiLog(
|
|
@@ -15708,6 +15838,9 @@ class BaseAPI {
|
|
|
15708
15838
|
} catch (e) {
|
|
15709
15839
|
returnValue = this.handleValueAccessException(CMIElement, e, returnValue);
|
|
15710
15840
|
}
|
|
15841
|
+
if (this.lastErrorCode === "0") {
|
|
15842
|
+
this.checkUninitializedGet(CMIElement, returnValue);
|
|
15843
|
+
}
|
|
15711
15844
|
this.processListeners(callbackName, CMIElement);
|
|
15712
15845
|
}
|
|
15713
15846
|
this.apiLog(callbackName, ": returned: " + returnValue, LogLevelEnum.INFO, CMIElement);
|
|
@@ -15717,6 +15850,10 @@ class BaseAPI {
|
|
|
15717
15850
|
if (this.lastErrorCode === "0") {
|
|
15718
15851
|
this.clearSCORMError(returnValue);
|
|
15719
15852
|
}
|
|
15853
|
+
const rawReturn = returnValue;
|
|
15854
|
+
if (typeof rawReturn === "number" || typeof rawReturn === "boolean") {
|
|
15855
|
+
return String(rawReturn);
|
|
15856
|
+
}
|
|
15720
15857
|
return returnValue;
|
|
15721
15858
|
}
|
|
15722
15859
|
/**
|
|
@@ -15769,9 +15906,10 @@ class BaseAPI {
|
|
|
15769
15906
|
* Orders LMS to store all content parameters
|
|
15770
15907
|
* @param {string} callbackName
|
|
15771
15908
|
* @param {boolean} checkTerminated
|
|
15909
|
+
* @param {CommitTrigger} trigger - What initiated the commit
|
|
15772
15910
|
* @return {string}
|
|
15773
15911
|
*/
|
|
15774
|
-
commit(callbackName, checkTerminated = false) {
|
|
15912
|
+
commit(callbackName, checkTerminated = false, trigger = "manual") {
|
|
15775
15913
|
this.clearScheduledCommit();
|
|
15776
15914
|
let returnValue = global_constants.SCORM_TRUE;
|
|
15777
15915
|
if (this.isNotInitialized()) {
|
|
@@ -15783,7 +15921,7 @@ class BaseAPI {
|
|
|
15783
15921
|
this.throwSCORMError("api", errorCode);
|
|
15784
15922
|
if (errorCode === 143) returnValue = global_constants.SCORM_FALSE;
|
|
15785
15923
|
} else {
|
|
15786
|
-
const result = this.storeData(false);
|
|
15924
|
+
const result = this.storeData(false, trigger);
|
|
15787
15925
|
const errorCode = result.errorCode ?? 0;
|
|
15788
15926
|
if (errorCode > 0) {
|
|
15789
15927
|
if (result.errorMessage) {
|
|
@@ -16014,7 +16152,16 @@ class BaseAPI {
|
|
|
16014
16152
|
* @return {string}
|
|
16015
16153
|
*/
|
|
16016
16154
|
_commonSetCMIValue(methodName, scorm2004, CMIElement, value) {
|
|
16017
|
-
|
|
16155
|
+
const result = this._cmiValueAccessService.setCMIValue(
|
|
16156
|
+
methodName,
|
|
16157
|
+
scorm2004,
|
|
16158
|
+
CMIElement,
|
|
16159
|
+
value
|
|
16160
|
+
);
|
|
16161
|
+
if (result === global_constants.SCORM_TRUE) {
|
|
16162
|
+
this._setCMIElements.add(CMIElement);
|
|
16163
|
+
}
|
|
16164
|
+
return result;
|
|
16018
16165
|
}
|
|
16019
16166
|
/**
|
|
16020
16167
|
* Gets a value from the CMI Object.
|
|
@@ -16028,6 +16175,18 @@ class BaseAPI {
|
|
|
16028
16175
|
_commonGetCMIValue(methodName, scorm2004, CMIElement) {
|
|
16029
16176
|
return this._cmiValueAccessService.getCMIValue(methodName, scorm2004, CMIElement);
|
|
16030
16177
|
}
|
|
16178
|
+
/**
|
|
16179
|
+
* Hook invoked by getValue after a successful resolution. Standards that must
|
|
16180
|
+
* distinguish "implemented but never set, no default value" from a
|
|
16181
|
+
* legitimately empty value override this to raise VALUE_NOT_INITIALIZED.
|
|
16182
|
+
* Default is a no-op, so SCORM 1.2 / AICC keep returning "" with code 0.
|
|
16183
|
+
*
|
|
16184
|
+
* @param {string} _CMIElement - the element that was read
|
|
16185
|
+
* @param {any} _returnValue - the value getCMIValue resolved
|
|
16186
|
+
* @protected
|
|
16187
|
+
*/
|
|
16188
|
+
checkUninitializedGet(_CMIElement, _returnValue) {
|
|
16189
|
+
}
|
|
16031
16190
|
/**
|
|
16032
16191
|
* Returns true if the API's current state is STATE_INITIALIZED
|
|
16033
16192
|
*
|
|
@@ -16110,9 +16269,14 @@ class BaseAPI {
|
|
|
16110
16269
|
* @param {string} functionName - The name of the function/event that occurred
|
|
16111
16270
|
* @param {string} CMIElement - Optional CMI element involved in the event
|
|
16112
16271
|
* @param {any} value - Optional value associated with the event
|
|
16272
|
+
* @param {CommitEventContext} context - Optional context for commit lifecycle events
|
|
16113
16273
|
*/
|
|
16114
|
-
processListeners(functionName, CMIElement, value) {
|
|
16115
|
-
|
|
16274
|
+
processListeners(functionName, CMIElement, value, context) {
|
|
16275
|
+
if (context !== void 0) {
|
|
16276
|
+
this._eventService.processListeners(functionName, CMIElement, value, context);
|
|
16277
|
+
} else {
|
|
16278
|
+
this._eventService.processListeners(functionName, CMIElement, value);
|
|
16279
|
+
}
|
|
16116
16280
|
}
|
|
16117
16281
|
/**
|
|
16118
16282
|
* Throws a SCORM error with the specified error number and optional message.
|
|
@@ -16245,36 +16409,106 @@ class BaseAPI {
|
|
|
16245
16409
|
* @param {string} url - The URL to send the request to
|
|
16246
16410
|
* @param {CommitObject | StringKeyMap | Array<any>} params - The parameters to send
|
|
16247
16411
|
* @param {boolean} immediate - Whether to send the request immediately without waiting
|
|
16412
|
+
* @param {CommitTrigger} [trigger] - What initiated the commit
|
|
16248
16413
|
* @returns {ResultObject} - The result of the request
|
|
16249
16414
|
*/
|
|
16250
|
-
processHttpRequest(url, params, immediate = false) {
|
|
16251
|
-
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
|
|
16255
|
-
|
|
16256
|
-
)
|
|
16257
|
-
|
|
16258
|
-
|
|
16259
|
-
|
|
16415
|
+
processHttpRequest(url, params, immediate = false, trigger) {
|
|
16416
|
+
const sequence = ++this._commitSequence;
|
|
16417
|
+
this._pendingCommitCount += 1;
|
|
16418
|
+
let settled = false;
|
|
16419
|
+
let completionDeferred = false;
|
|
16420
|
+
const settle = () => {
|
|
16421
|
+
if (settled) {
|
|
16422
|
+
return;
|
|
16423
|
+
}
|
|
16424
|
+
settled = true;
|
|
16425
|
+
this._pendingCommitCount -= 1;
|
|
16426
|
+
if (this._pendingCommitCount === 0) {
|
|
16427
|
+
this._flushCommitSettleWaiters();
|
|
16428
|
+
}
|
|
16429
|
+
};
|
|
16430
|
+
try {
|
|
16431
|
+
const resolvedTrigger = trigger ?? (immediate ? "terminate" : "manual");
|
|
16432
|
+
let finalParams = params;
|
|
16433
|
+
if (immediate && this.settings.terminateCommitPayloadField) {
|
|
16434
|
+
const field = this.settings.terminateCommitPayloadField;
|
|
16435
|
+
if (Array.isArray(finalParams)) {
|
|
16436
|
+
finalParams = [...finalParams, `${encodeURIComponent(field)}=true`];
|
|
16437
|
+
} else if (finalParams && typeof finalParams === "object") {
|
|
16438
|
+
finalParams = { ...finalParams, [field]: true };
|
|
16439
|
+
}
|
|
16440
|
+
}
|
|
16441
|
+
if (this.settings.includeCommitSequence === true) {
|
|
16442
|
+
if (Array.isArray(finalParams)) {
|
|
16443
|
+
finalParams = [...finalParams, `commitSequence=${sequence}`];
|
|
16444
|
+
} else if (finalParams && typeof finalParams === "object") {
|
|
16445
|
+
finalParams = { ...finalParams, commitSequence: sequence };
|
|
16446
|
+
}
|
|
16447
|
+
}
|
|
16448
|
+
const finalUrl = immediate && this.settings.terminateCommitParam ? appendQueryParam(url, this.settings.terminateCommitParam, "true") : url;
|
|
16449
|
+
const metadata = {
|
|
16450
|
+
isTerminateCommit: immediate,
|
|
16451
|
+
trigger: resolvedTrigger,
|
|
16452
|
+
sequence
|
|
16453
|
+
};
|
|
16454
|
+
const context = {
|
|
16455
|
+
url: finalUrl,
|
|
16456
|
+
trigger: resolvedTrigger,
|
|
16457
|
+
isTerminateCommit: immediate,
|
|
16458
|
+
sequence
|
|
16459
|
+
};
|
|
16460
|
+
if (this.settings.enableOfflineSupport && this._offlineStorageService && !this._offlineStorageService.isDeviceOnline() && this._courseId) {
|
|
16260
16461
|
this.apiLog(
|
|
16261
16462
|
"processHttpRequest",
|
|
16262
|
-
"
|
|
16263
|
-
LogLevelEnum.
|
|
16463
|
+
"Device is offline, storing data locally",
|
|
16464
|
+
LogLevelEnum.INFO
|
|
16264
16465
|
);
|
|
16265
|
-
|
|
16266
|
-
|
|
16267
|
-
|
|
16268
|
-
|
|
16466
|
+
if (finalParams && typeof finalParams === "object" && "cmi" in finalParams) {
|
|
16467
|
+
return this._offlineStorageService.storeOffline(
|
|
16468
|
+
this._courseId,
|
|
16469
|
+
finalParams,
|
|
16470
|
+
{ isTerminateCommit: immediate, sequence }
|
|
16471
|
+
);
|
|
16472
|
+
} else {
|
|
16473
|
+
this.apiLog(
|
|
16474
|
+
"processHttpRequest",
|
|
16475
|
+
"Invalid commit data format for offline storage",
|
|
16476
|
+
LogLevelEnum.ERROR
|
|
16477
|
+
);
|
|
16478
|
+
return {
|
|
16479
|
+
result: global_constants.SCORM_FALSE,
|
|
16480
|
+
errorCode: this._error_codes.GENERAL ?? 101
|
|
16481
|
+
};
|
|
16482
|
+
}
|
|
16483
|
+
}
|
|
16484
|
+
const apiLog = (functionName, message, level, element) => this.apiLog(functionName, message, level, element);
|
|
16485
|
+
const processListeners = (functionName, CMIElement, value) => {
|
|
16486
|
+
if (functionName === "CommitSuccess" || functionName === "CommitError") {
|
|
16487
|
+
if (functionName === "CommitError" && typeof value === "number") {
|
|
16488
|
+
context.errorCode = value;
|
|
16489
|
+
}
|
|
16490
|
+
settle();
|
|
16491
|
+
this.processListeners(functionName, CMIElement, value, context);
|
|
16492
|
+
} else {
|
|
16493
|
+
this.processListeners(functionName, CMIElement, value);
|
|
16494
|
+
}
|
|
16495
|
+
};
|
|
16496
|
+
const result = this._httpService.processHttpRequest(
|
|
16497
|
+
finalUrl,
|
|
16498
|
+
finalParams,
|
|
16499
|
+
immediate,
|
|
16500
|
+
apiLog,
|
|
16501
|
+
processListeners,
|
|
16502
|
+
metadata,
|
|
16503
|
+
settle
|
|
16504
|
+
);
|
|
16505
|
+
completionDeferred = this._httpService.reportsRequestCompletion === true;
|
|
16506
|
+
return result;
|
|
16507
|
+
} finally {
|
|
16508
|
+
if (!completionDeferred) {
|
|
16509
|
+
settle();
|
|
16269
16510
|
}
|
|
16270
16511
|
}
|
|
16271
|
-
return this._httpService.processHttpRequest(
|
|
16272
|
-
url,
|
|
16273
|
-
params,
|
|
16274
|
-
immediate,
|
|
16275
|
-
(functionName, message, level, element) => this.apiLog(functionName, message, level, element),
|
|
16276
|
-
(functionName, CMIElement, value) => this.processListeners(functionName, CMIElement, value)
|
|
16277
|
-
);
|
|
16278
16512
|
}
|
|
16279
16513
|
/**
|
|
16280
16514
|
* Schedules a commit operation to occur after a specified delay.
|
|
@@ -17258,6 +17492,11 @@ class CMIScore extends BaseCMI {
|
|
|
17258
17492
|
__invalid_range_code;
|
|
17259
17493
|
__decimal_regex;
|
|
17260
17494
|
__error_class;
|
|
17495
|
+
/**
|
|
17496
|
+
* When true, an empty string is a valid value (clears the element). SCORM 1.2
|
|
17497
|
+
* score elements may be blank per the ADL 1.2 CTS; SCORM 2004 leaves this off.
|
|
17498
|
+
*/
|
|
17499
|
+
__allow_empty_string;
|
|
17261
17500
|
_raw = "";
|
|
17262
17501
|
_min = "";
|
|
17263
17502
|
_max;
|
|
@@ -17292,6 +17531,7 @@ class CMIScore extends BaseCMI {
|
|
|
17292
17531
|
this.__invalid_range_code = params.invalidRangeCode || scorm12_errors.VALUE_OUT_OF_RANGE;
|
|
17293
17532
|
this.__decimal_regex = params.decimalRegex || scorm12_regex.CMIDecimal;
|
|
17294
17533
|
this.__error_class = params.errorClass;
|
|
17534
|
+
this.__allow_empty_string = params.allowEmptyString ?? false;
|
|
17295
17535
|
}
|
|
17296
17536
|
/**
|
|
17297
17537
|
* Called when the API has been reset
|
|
@@ -17338,7 +17578,8 @@ class CMIScore extends BaseCMI {
|
|
|
17338
17578
|
this.__score_range,
|
|
17339
17579
|
this.__invalid_type_code,
|
|
17340
17580
|
this.__invalid_range_code,
|
|
17341
|
-
this.__error_class
|
|
17581
|
+
this.__error_class,
|
|
17582
|
+
this.__allow_empty_string
|
|
17342
17583
|
)) {
|
|
17343
17584
|
this._raw = raw;
|
|
17344
17585
|
}
|
|
@@ -17362,7 +17603,8 @@ class CMIScore extends BaseCMI {
|
|
|
17362
17603
|
this.__score_range,
|
|
17363
17604
|
this.__invalid_type_code,
|
|
17364
17605
|
this.__invalid_range_code,
|
|
17365
|
-
this.__error_class
|
|
17606
|
+
this.__error_class,
|
|
17607
|
+
this.__allow_empty_string
|
|
17366
17608
|
)) {
|
|
17367
17609
|
this._min = min;
|
|
17368
17610
|
}
|
|
@@ -17386,7 +17628,8 @@ class CMIScore extends BaseCMI {
|
|
|
17386
17628
|
this.__score_range,
|
|
17387
17629
|
this.__invalid_type_code,
|
|
17388
17630
|
this.__invalid_range_code,
|
|
17389
|
-
this.__error_class
|
|
17631
|
+
this.__error_class,
|
|
17632
|
+
this.__allow_empty_string
|
|
17390
17633
|
)) {
|
|
17391
17634
|
this._max = max;
|
|
17392
17635
|
}
|
|
@@ -22063,19 +22306,13 @@ class Scorm2004DataSerializer {
|
|
|
22063
22306
|
} else {
|
|
22064
22307
|
delete cmiExport.cmi.total_time;
|
|
22065
22308
|
}
|
|
22066
|
-
const result = [];
|
|
22067
22309
|
const flattened = flatten(cmiExport);
|
|
22068
22310
|
const settings = this.context.getSettings();
|
|
22069
22311
|
switch (settings.dataCommitFormat) {
|
|
22070
22312
|
case "flattened":
|
|
22071
|
-
return
|
|
22313
|
+
return flattened;
|
|
22072
22314
|
case "params":
|
|
22073
|
-
|
|
22074
|
-
if ({}.hasOwnProperty.call(flattened, item)) {
|
|
22075
|
-
result.push(`${item}=${flattened[item]}`);
|
|
22076
|
-
}
|
|
22077
|
-
}
|
|
22078
|
-
return result;
|
|
22315
|
+
return Object.entries(flattened).map(([item, value]) => `${item}=${value}`);
|
|
22079
22316
|
case "json":
|
|
22080
22317
|
default:
|
|
22081
22318
|
return cmiExport;
|
|
@@ -22180,6 +22417,35 @@ class Scorm2004DataSerializer {
|
|
|
22180
22417
|
}
|
|
22181
22418
|
}
|
|
22182
22419
|
|
|
22420
|
+
const NO_DEFAULT_2004_ELEMENTS = /* @__PURE__ */ new Set([
|
|
22421
|
+
"cmi.suspend_data",
|
|
22422
|
+
"cmi.location",
|
|
22423
|
+
"cmi.scaled_passing_score",
|
|
22424
|
+
"cmi.max_time_allowed",
|
|
22425
|
+
"cmi.completion_threshold",
|
|
22426
|
+
"cmi.progress_measure",
|
|
22427
|
+
"cmi.score.scaled",
|
|
22428
|
+
"cmi.score.raw",
|
|
22429
|
+
"cmi.score.min",
|
|
22430
|
+
"cmi.score.max",
|
|
22431
|
+
"cmi.objectives.N.score.scaled",
|
|
22432
|
+
"cmi.objectives.N.score.raw",
|
|
22433
|
+
"cmi.objectives.N.score.min",
|
|
22434
|
+
"cmi.objectives.N.score.max",
|
|
22435
|
+
"cmi.objectives.N.progress_measure",
|
|
22436
|
+
"cmi.objectives.N.description",
|
|
22437
|
+
"cmi.interactions.N.weighting",
|
|
22438
|
+
"cmi.interactions.N.type",
|
|
22439
|
+
"cmi.interactions.N.timestamp",
|
|
22440
|
+
"cmi.interactions.N.result",
|
|
22441
|
+
"cmi.interactions.N.latency",
|
|
22442
|
+
"cmi.interactions.N.learner_response",
|
|
22443
|
+
"cmi.interactions.N.description",
|
|
22444
|
+
"cmi.comments_from_learner.N.timestamp"
|
|
22445
|
+
]);
|
|
22446
|
+
function normalizeCMIIndices(CMIElement) {
|
|
22447
|
+
return CMIElement.replace(/\.\d+(?=\.|$)/g, ".N");
|
|
22448
|
+
}
|
|
22183
22449
|
class Scorm2004API extends BaseAPI {
|
|
22184
22450
|
_version = "1.0";
|
|
22185
22451
|
_sequencing;
|
|
@@ -22705,6 +22971,39 @@ class Scorm2004API extends BaseAPI {
|
|
|
22705
22971
|
getCMIValue(CMIElement) {
|
|
22706
22972
|
return this._commonGetCMIValue("GetValue", true, CMIElement);
|
|
22707
22973
|
}
|
|
22974
|
+
/**
|
|
22975
|
+
* Raises 403 (VALUE_NOT_INITIALIZED) when an implemented, no-default element is
|
|
22976
|
+
* read before it has ever been set, per IEEE 1484.11.2 / SCORM 2004 RTE.
|
|
22977
|
+
*
|
|
22978
|
+
* Invoked by BaseAPI.getValue after an otherwise-successful resolution, so it
|
|
22979
|
+
* only sees values the data model already returned cleanly. The decision is
|
|
22980
|
+
* deliberately gated to this public boundary (never the getters) to keep
|
|
22981
|
+
* serialization/commit/rollup — which read the getters directly — unaffected.
|
|
22982
|
+
*
|
|
22983
|
+
* A 403 is raised only when all hold:
|
|
22984
|
+
* - the resolved value is "" — any non-empty value is by definition set,
|
|
22985
|
+
* including values written by internal sequencing/activity-tree paths that
|
|
22986
|
+
* bypass _commonSetCMIValue (those only ever write non-empty values);
|
|
22987
|
+
* - the element was never set — _setCMIElements records every successful
|
|
22988
|
+
* SetValue, loadFromJSON, and global-objective restore (all route through
|
|
22989
|
+
* _commonSetCMIValue), so an explicit SetValue(x, "") still reads back as
|
|
22990
|
+
* "" with code 0; and
|
|
22991
|
+
* - the element has no spec-defined default ({@link NO_DEFAULT_2004_ELEMENTS}).
|
|
22992
|
+
*
|
|
22993
|
+
* @param {string} CMIElement
|
|
22994
|
+
* @param {*} returnValue - the value getCMIValue resolved
|
|
22995
|
+
* @protected
|
|
22996
|
+
*/
|
|
22997
|
+
checkUninitializedGet(CMIElement, returnValue) {
|
|
22998
|
+
if (returnValue !== "") return;
|
|
22999
|
+
if (this._setCMIElements.has(CMIElement)) return;
|
|
23000
|
+
if (!NO_DEFAULT_2004_ELEMENTS.has(normalizeCMIIndices(CMIElement))) return;
|
|
23001
|
+
this.throwSCORMError(
|
|
23002
|
+
CMIElement,
|
|
23003
|
+
this._error_codes.VALUE_NOT_INITIALIZED ?? 403,
|
|
23004
|
+
`The data model element passed to GetValue (${CMIElement}) has not been initialized.`
|
|
23005
|
+
);
|
|
23006
|
+
}
|
|
22708
23007
|
/**
|
|
22709
23008
|
* Returns the message that corresponds to errorNumber.
|
|
22710
23009
|
* @param {(string|number)} errorNumber
|
|
@@ -22751,9 +23050,10 @@ class Scorm2004API extends BaseAPI {
|
|
|
22751
23050
|
/**
|
|
22752
23051
|
* Attempts to store the data to the LMS
|
|
22753
23052
|
* @param {boolean} terminateCommit
|
|
23053
|
+
* @param {CommitTrigger} [trigger] - What initiated the commit
|
|
22754
23054
|
* @return {ResultObject}
|
|
22755
23055
|
*/
|
|
22756
|
-
storeData(terminateCommit) {
|
|
23056
|
+
storeData(terminateCommit, trigger) {
|
|
22757
23057
|
if (terminateCommit) {
|
|
22758
23058
|
if (this.cmi.mode === "normal") {
|
|
22759
23059
|
if (this.cmi.credit === "credit") {
|
|
@@ -22801,7 +23101,8 @@ class Scorm2004API extends BaseAPI {
|
|
|
22801
23101
|
const result = this.processHttpRequest(
|
|
22802
23102
|
this.settings.lmsCommitUrl,
|
|
22803
23103
|
commitObject,
|
|
22804
|
-
terminateCommit
|
|
23104
|
+
terminateCommit,
|
|
23105
|
+
trigger
|
|
22805
23106
|
);
|
|
22806
23107
|
if (navRequest && result.navRequest !== void 0 && result.navRequest !== "" && typeof result.navRequest === "string") {
|
|
22807
23108
|
const parsed = parseNavigationRequest(result.navRequest);
|