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.
- package/README.md +77 -1
- package/dist/cross-frame-api.js.map +1 -1
- package/dist/esm/cross-frame-api.js.map +1 -1
- package/dist/esm/scorm-again.js +388 -71
- 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 +322 -69
- 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 +371 -64
- 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 +341 -55
- 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 +305 -53
- 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 +329 -48
- 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 +21 -21
package/dist/esm/scorm-again.js
CHANGED
|
@@ -268,6 +268,15 @@ function parseNavigationRequest(navRequest) {
|
|
|
268
268
|
};
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
const appendQueryParam = (url, name, value) => {
|
|
272
|
+
const fragmentIndex = url.indexOf("#");
|
|
273
|
+
const baseUrl = fragmentIndex === -1 ? url : url.slice(0, fragmentIndex);
|
|
274
|
+
const fragment = fragmentIndex === -1 ? "" : url.slice(fragmentIndex);
|
|
275
|
+
const separator = baseUrl.includes("?") ? baseUrl.endsWith("?") || baseUrl.endsWith("&") ? "" : "&" : "?";
|
|
276
|
+
const queryParam = `${encodeURIComponent(name)}=${encodeURIComponent(String(value))}`;
|
|
277
|
+
return `${baseUrl}${separator}${queryParam}${fragment}`;
|
|
278
|
+
};
|
|
279
|
+
|
|
271
280
|
class BaseCMI {
|
|
272
281
|
/**
|
|
273
282
|
* Flag used during JSON serialization to allow getter access without initialization checks.
|
|
@@ -659,7 +668,10 @@ const scorm12_errors = {
|
|
|
659
668
|
READ_ONLY_ELEMENT: 403,
|
|
660
669
|
WRITE_ONLY_ELEMENT: 404,
|
|
661
670
|
TYPE_MISMATCH: 405,
|
|
662
|
-
|
|
671
|
+
// SCORM 1.2 has no out-of-range error code; an out-of-range value is reported
|
|
672
|
+
// as 405 (incorrect data type), per the SCORM 1.2 RTE error table and the ADL
|
|
673
|
+
// 1.2 CTS (DataModelValidator.checkScoreDecimal failure -> CMICategory 405).
|
|
674
|
+
VALUE_OUT_OF_RANGE: 405,
|
|
663
675
|
DEPENDENCY_NOT_ESTABLISHED: 408
|
|
664
676
|
};
|
|
665
677
|
const scorm2004_errors = {
|
|
@@ -807,6 +819,7 @@ const DefaultSettings = {
|
|
|
807
819
|
xhrWithCredentials: false,
|
|
808
820
|
fetchMode: "cors",
|
|
809
821
|
asyncModeBeaconBehavior: "never",
|
|
822
|
+
includeCommitSequence: false,
|
|
810
823
|
responseHandler: async function(response) {
|
|
811
824
|
if (typeof response !== "undefined") {
|
|
812
825
|
let httpResult = null;
|
|
@@ -1739,7 +1752,7 @@ class ScheduledCommit {
|
|
|
1739
1752
|
wrapper() {
|
|
1740
1753
|
if (!this._cancelled) {
|
|
1741
1754
|
if (this._API.isInitialized()) {
|
|
1742
|
-
(async () => await this._API.commit(this._callback))();
|
|
1755
|
+
(async () => await this._API.commit(this._callback, false, "autocommit"))();
|
|
1743
1756
|
}
|
|
1744
1757
|
}
|
|
1745
1758
|
}
|
|
@@ -5084,6 +5097,7 @@ class ActivityDeliveryService {
|
|
|
5084
5097
|
}
|
|
5085
5098
|
|
|
5086
5099
|
class AsynchronousHttpService {
|
|
5100
|
+
reportsRequestCompletion = true;
|
|
5087
5101
|
settings;
|
|
5088
5102
|
error_codes;
|
|
5089
5103
|
/**
|
|
@@ -5108,10 +5122,20 @@ class AsynchronousHttpService {
|
|
|
5108
5122
|
* @param {boolean} immediate - Whether to send the request immediately without waiting
|
|
5109
5123
|
* @param {Function} apiLog - Function to log API messages with appropriate levels
|
|
5110
5124
|
* @param {Function} processListeners - Function to trigger event listeners for commit events
|
|
5125
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
5126
|
+
* @param {Function} onRequestComplete - Callback invoked after the background request settles
|
|
5111
5127
|
* @return {ResultObject} - Immediate optimistic success result
|
|
5112
5128
|
*/
|
|
5113
|
-
processHttpRequest(url, params, immediate = false, apiLog, processListeners) {
|
|
5114
|
-
this._performAsyncRequest(
|
|
5129
|
+
processHttpRequest(url, params, immediate = false, apiLog, processListeners, metadata, onRequestComplete) {
|
|
5130
|
+
this._performAsyncRequest(
|
|
5131
|
+
url,
|
|
5132
|
+
params,
|
|
5133
|
+
immediate,
|
|
5134
|
+
apiLog,
|
|
5135
|
+
processListeners,
|
|
5136
|
+
metadata,
|
|
5137
|
+
onRequestComplete
|
|
5138
|
+
);
|
|
5115
5139
|
return {
|
|
5116
5140
|
result: global_constants.SCORM_TRUE,
|
|
5117
5141
|
errorCode: 0
|
|
@@ -5124,11 +5148,14 @@ class AsynchronousHttpService {
|
|
|
5124
5148
|
* @param {boolean} immediate - Whether this is an immediate request
|
|
5125
5149
|
* @param apiLog - Function to log API messages
|
|
5126
5150
|
* @param {Function} processListeners - Function to process event listeners
|
|
5151
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
5152
|
+
* @param {Function} onRequestComplete - Callback invoked after the request settles
|
|
5127
5153
|
* @private
|
|
5128
5154
|
*/
|
|
5129
|
-
async _performAsyncRequest(url, params, immediate, apiLog, processListeners) {
|
|
5155
|
+
async _performAsyncRequest(url, params, immediate, apiLog, processListeners, metadata, onRequestComplete) {
|
|
5130
5156
|
try {
|
|
5131
|
-
const
|
|
5157
|
+
const handledParams = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
|
|
5158
|
+
const processedParams = handledParams;
|
|
5132
5159
|
let response;
|
|
5133
5160
|
if (immediate && this.settings.asyncModeBeaconBehavior !== "never") {
|
|
5134
5161
|
response = await this.performBeacon(url, processedParams);
|
|
@@ -5144,7 +5171,9 @@ class AsynchronousHttpService {
|
|
|
5144
5171
|
} catch (e) {
|
|
5145
5172
|
const message = e instanceof Error ? e.message : String(e);
|
|
5146
5173
|
apiLog("processHttpRequest", `Async request failed: ${message}`, LogLevelEnum.ERROR);
|
|
5147
|
-
processListeners("CommitError");
|
|
5174
|
+
processListeners("CommitError", void 0, this.error_codes.GENERAL_COMMIT_FAILURE || 391);
|
|
5175
|
+
} finally {
|
|
5176
|
+
onRequestComplete?.();
|
|
5148
5177
|
}
|
|
5149
5178
|
}
|
|
5150
5179
|
/**
|
|
@@ -5287,10 +5316,13 @@ class CMIValueAccessService {
|
|
|
5287
5316
|
}
|
|
5288
5317
|
/**
|
|
5289
5318
|
* Gets the appropriate error code for undefined data model elements.
|
|
5290
|
-
* SCORM 2004
|
|
5319
|
+
* Both SCORM 2004 and SCORM 1.2 use UNDEFINED_DATA_MODEL (401): an
|
|
5320
|
+
* unrecognized element is "Not implemented", not a general exception. SCORM
|
|
5321
|
+
* 1.2 previously returned GENERAL (101) here, which is non-conformant — the
|
|
5322
|
+
* ADL 1.2 CTS and the SCORM 1.2 RTE spec expect 401 for an unknown element.
|
|
5291
5323
|
*/
|
|
5292
|
-
getUndefinedDataModelErrorCode(
|
|
5293
|
-
return
|
|
5324
|
+
getUndefinedDataModelErrorCode() {
|
|
5325
|
+
return getErrorCode(this.context.errorCodes, "UNDEFINED_DATA_MODEL");
|
|
5294
5326
|
}
|
|
5295
5327
|
/**
|
|
5296
5328
|
* Sets a value on a CMI element path
|
|
@@ -5318,7 +5350,7 @@ class CMIValueAccessService {
|
|
|
5318
5350
|
let returnValue = global_constants.SCORM_FALSE;
|
|
5319
5351
|
let foundFirstIndex = false;
|
|
5320
5352
|
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
|
|
5321
|
-
const invalidErrorCode = this.getUndefinedDataModelErrorCode(
|
|
5353
|
+
const invalidErrorCode = this.getUndefinedDataModelErrorCode();
|
|
5322
5354
|
for (let idx = 0; idx < structure.length; idx++) {
|
|
5323
5355
|
const attribute = structure[idx];
|
|
5324
5356
|
if (idx === structure.length - 1) {
|
|
@@ -5388,12 +5420,13 @@ class CMIValueAccessService {
|
|
|
5388
5420
|
);
|
|
5389
5421
|
return "";
|
|
5390
5422
|
}
|
|
5423
|
+
this.context.setLastErrorCode("0");
|
|
5391
5424
|
const structure = CMIElement.split(".");
|
|
5392
5425
|
let refObject = this.context.getDataModel();
|
|
5393
5426
|
let attribute = null;
|
|
5394
5427
|
const uninitializedErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) has not been initialized.`;
|
|
5395
5428
|
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
|
|
5396
|
-
const invalidErrorCode = this.getUndefinedDataModelErrorCode(
|
|
5429
|
+
const invalidErrorCode = this.getUndefinedDataModelErrorCode();
|
|
5397
5430
|
for (let idx = 0; idx < structure.length; idx++) {
|
|
5398
5431
|
attribute = structure[idx];
|
|
5399
5432
|
const validationResult = this.validateGetAttribute(
|
|
@@ -6105,8 +6138,9 @@ class EventService {
|
|
|
6105
6138
|
* @param {string} functionName - The name of the function that triggered the event
|
|
6106
6139
|
* @param {string} CMIElement - The CMI element that was affected
|
|
6107
6140
|
* @param {any} value - The value that was set
|
|
6141
|
+
* @param {CommitEventContext} context - Optional context for commit lifecycle events
|
|
6108
6142
|
*/
|
|
6109
|
-
processListeners(functionName, CMIElement, value) {
|
|
6143
|
+
processListeners(functionName, CMIElement, value, context) {
|
|
6110
6144
|
this.apiLog(functionName, value, LogLevelEnum.INFO, CMIElement);
|
|
6111
6145
|
const listeners = this.listenerMap.get(functionName);
|
|
6112
6146
|
if (!listeners) return;
|
|
@@ -6131,9 +6165,17 @@ class EventService {
|
|
|
6131
6165
|
if (functionName.startsWith("Sequence")) {
|
|
6132
6166
|
listener.callback(value);
|
|
6133
6167
|
} else if (functionName === "CommitError") {
|
|
6134
|
-
|
|
6168
|
+
if (context !== void 0) {
|
|
6169
|
+
listener.callback(value, context);
|
|
6170
|
+
} else {
|
|
6171
|
+
listener.callback(value);
|
|
6172
|
+
}
|
|
6135
6173
|
} else if (functionName === "CommitSuccess") {
|
|
6136
|
-
|
|
6174
|
+
if (context !== void 0) {
|
|
6175
|
+
listener.callback(context);
|
|
6176
|
+
} else {
|
|
6177
|
+
listener.callback();
|
|
6178
|
+
}
|
|
6137
6179
|
} else {
|
|
6138
6180
|
listener.callback(CMIElement, value);
|
|
6139
6181
|
}
|
|
@@ -6267,16 +6309,19 @@ class OfflineStorageService {
|
|
|
6267
6309
|
* Store commit data offline
|
|
6268
6310
|
* @param {string} courseId - Identifier for the course
|
|
6269
6311
|
* @param {CommitObject} commitData - The data to store offline
|
|
6312
|
+
* @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
|
|
6270
6313
|
* @returns {ResultObject} - Result of the storage operation
|
|
6271
6314
|
*/
|
|
6272
|
-
storeOffline(courseId, commitData) {
|
|
6315
|
+
storeOffline(courseId, commitData, metadata) {
|
|
6273
6316
|
try {
|
|
6274
6317
|
const queueItem = {
|
|
6275
6318
|
id: `${courseId}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
|
6276
6319
|
courseId,
|
|
6277
6320
|
timestamp: Date.now(),
|
|
6278
6321
|
data: commitData,
|
|
6279
|
-
syncAttempts: 0
|
|
6322
|
+
syncAttempts: 0,
|
|
6323
|
+
...metadata?.isTerminateCommit !== void 0 ? { isTerminateCommit: metadata.isTerminateCommit } : {},
|
|
6324
|
+
...metadata?.sequence !== void 0 ? { sequence: metadata.sequence } : {}
|
|
6280
6325
|
};
|
|
6281
6326
|
const currentQueue = this.getFromStorage(this.syncQueue) || [];
|
|
6282
6327
|
currentQueue.push(queueItem);
|
|
@@ -6355,7 +6400,10 @@ class OfflineStorageService {
|
|
|
6355
6400
|
continue;
|
|
6356
6401
|
}
|
|
6357
6402
|
try {
|
|
6358
|
-
const syncResult = await this.sendDataToLMS(item.data
|
|
6403
|
+
const syncResult = await this.sendDataToLMS(item.data, {
|
|
6404
|
+
...item.isTerminateCommit !== void 0 ? { isTerminateCommit: item.isTerminateCommit } : {},
|
|
6405
|
+
...item.sequence !== void 0 ? { sequence: item.sequence } : {}
|
|
6406
|
+
});
|
|
6359
6407
|
if (syncResult.result === true || syncResult.result === global_constants.SCORM_TRUE) {
|
|
6360
6408
|
this.apiLog(
|
|
6361
6409
|
"OfflineStorageService",
|
|
@@ -6402,17 +6450,25 @@ class OfflineStorageService {
|
|
|
6402
6450
|
/**
|
|
6403
6451
|
* Send data to the LMS when online
|
|
6404
6452
|
* @param {CommitObject} data - The data to send to the LMS
|
|
6453
|
+
* @param {OfflineCommitMetadata} metadata - Metadata captured with the original commit
|
|
6405
6454
|
* @returns {Promise<ResultObject>} - Result of the sync operation
|
|
6406
6455
|
*/
|
|
6407
|
-
async sendDataToLMS(data) {
|
|
6408
|
-
|
|
6456
|
+
async sendDataToLMS(data, metadata) {
|
|
6457
|
+
const configuredCommitUrl = this.settings.lmsCommitUrl;
|
|
6458
|
+
if (!configuredCommitUrl) {
|
|
6409
6459
|
return {
|
|
6410
6460
|
result: global_constants.SCORM_FALSE,
|
|
6411
6461
|
errorCode: this.error_codes.GENERAL || 101
|
|
6412
6462
|
};
|
|
6413
6463
|
}
|
|
6414
6464
|
try {
|
|
6415
|
-
const
|
|
6465
|
+
const lmsCommitUrl = String(configuredCommitUrl);
|
|
6466
|
+
const processedData = this.settings.requestHandler(data, {
|
|
6467
|
+
isTerminateCommit: metadata?.isTerminateCommit ?? false,
|
|
6468
|
+
trigger: "offline-replay",
|
|
6469
|
+
...metadata?.sequence !== void 0 ? { sequence: metadata.sequence } : {}
|
|
6470
|
+
});
|
|
6471
|
+
const requestUrl = metadata?.isTerminateCommit && this.settings.terminateCommitParam ? appendQueryParam(lmsCommitUrl, this.settings.terminateCommitParam, "true") : lmsCommitUrl;
|
|
6416
6472
|
const init = {
|
|
6417
6473
|
method: "POST",
|
|
6418
6474
|
mode: this.settings.fetchMode,
|
|
@@ -6425,7 +6481,7 @@ class OfflineStorageService {
|
|
|
6425
6481
|
if (this.settings.xhrWithCredentials) {
|
|
6426
6482
|
init.credentials = "include";
|
|
6427
6483
|
}
|
|
6428
|
-
const response = await fetch(
|
|
6484
|
+
const response = await fetch(requestUrl, init);
|
|
6429
6485
|
const result = typeof this.settings.responseHandler === "function" ? await this.settings.responseHandler(response) : await response.json();
|
|
6430
6486
|
if (response.status >= 200 && response.status <= 299 && (result.result === true || result.result === global_constants.SCORM_TRUE)) {
|
|
6431
6487
|
if (!Object.hasOwnProperty.call(result, "errorCode")) {
|
|
@@ -15390,6 +15446,8 @@ class SynchronousHttpService {
|
|
|
15390
15446
|
* @param {boolean} immediate - Whether this is a termination commit (use sendBeacon)
|
|
15391
15447
|
* @param {Function} _apiLog - Function to log API messages (unused in synchronous mode - errors returned directly)
|
|
15392
15448
|
* @param {Function} _processListeners - Function to trigger event listeners (unused in synchronous mode - no async events)
|
|
15449
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
15450
|
+
* @param {Function} _onRequestComplete - Completion callback (unused because requests settle before return)
|
|
15393
15451
|
* @return {ResultObject} - The result of the request (synchronous)
|
|
15394
15452
|
*
|
|
15395
15453
|
* @remarks
|
|
@@ -15399,21 +15457,23 @@ class SynchronousHttpService {
|
|
|
15399
15457
|
* - No async events need to be triggered (CommitSuccess/CommitError) since results are synchronous
|
|
15400
15458
|
* - AsynchronousHttpService uses these parameters to handle background request results
|
|
15401
15459
|
*/
|
|
15402
|
-
processHttpRequest(url, params, immediate = false, _apiLog, _processListeners) {
|
|
15460
|
+
processHttpRequest(url, params, immediate = false, _apiLog, _processListeners, metadata, _onRequestComplete) {
|
|
15403
15461
|
if (immediate) {
|
|
15404
|
-
return this._handleImmediateRequest(url, params);
|
|
15462
|
+
return this._handleImmediateRequest(url, params, metadata);
|
|
15405
15463
|
}
|
|
15406
|
-
return this._performSyncXHR(url, params);
|
|
15464
|
+
return this._performSyncXHR(url, params, metadata);
|
|
15407
15465
|
}
|
|
15408
15466
|
/**
|
|
15409
15467
|
* Handles an immediate request using sendBeacon
|
|
15410
15468
|
* @param {string} url - The URL to send the request to
|
|
15411
15469
|
* @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
|
|
15470
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
15412
15471
|
* @return {ResultObject} - The result based on beacon success
|
|
15413
15472
|
* @private
|
|
15414
15473
|
*/
|
|
15415
|
-
_handleImmediateRequest(url, params) {
|
|
15416
|
-
const
|
|
15474
|
+
_handleImmediateRequest(url, params, metadata) {
|
|
15475
|
+
const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
|
|
15476
|
+
const requestPayload = handledPayload ?? params;
|
|
15417
15477
|
const { body } = this._prepareRequestBody(requestPayload);
|
|
15418
15478
|
const beaconSuccess = navigator.sendBeacon(
|
|
15419
15479
|
url,
|
|
@@ -15428,11 +15488,13 @@ class SynchronousHttpService {
|
|
|
15428
15488
|
* Performs a synchronous XMLHttpRequest
|
|
15429
15489
|
* @param {string} url - The URL to send the request to
|
|
15430
15490
|
* @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
|
|
15491
|
+
* @param {CommitMetadata} metadata - Metadata describing the captured commit
|
|
15431
15492
|
* @return {ResultObject} - The result of the request
|
|
15432
15493
|
* @private
|
|
15433
15494
|
*/
|
|
15434
|
-
_performSyncXHR(url, params) {
|
|
15435
|
-
const
|
|
15495
|
+
_performSyncXHR(url, params, metadata) {
|
|
15496
|
+
const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
|
|
15497
|
+
const requestPayload = handledPayload ?? params;
|
|
15436
15498
|
const { body, contentType } = this._prepareRequestBody(requestPayload);
|
|
15437
15499
|
const xhr = new XMLHttpRequest();
|
|
15438
15500
|
xhr.open("POST", url, false);
|
|
@@ -15511,9 +15573,15 @@ class ValidationService {
|
|
|
15511
15573
|
* @param {number} invalidTypeCode - The error code for invalid type
|
|
15512
15574
|
* @param {number} invalidRangeCode - The error code for invalid range
|
|
15513
15575
|
* @param {typeof BaseScormValidationError} errorClass - The error class to use for validation errors
|
|
15576
|
+
* @param {boolean} allowEmptyString - When true, an empty string is accepted (clears the value).
|
|
15577
|
+
* SCORM 1.2 score elements may be blank per the ADL 1.2 CTS
|
|
15578
|
+
* (DataModelValidator.checkScoreDecimal treats blank as valid).
|
|
15514
15579
|
* @return {boolean} - True if validation passes, throws an error otherwise
|
|
15515
15580
|
*/
|
|
15516
|
-
validateScore(CMIElement, value, decimalRegex, scoreRange, invalidTypeCode, invalidRangeCode, errorClass) {
|
|
15581
|
+
validateScore(CMIElement, value, decimalRegex, scoreRange, invalidTypeCode, invalidRangeCode, errorClass, allowEmptyString = false) {
|
|
15582
|
+
if (allowEmptyString && value === "") {
|
|
15583
|
+
return true;
|
|
15584
|
+
}
|
|
15517
15585
|
return checkValidFormat(CMIElement, value, decimalRegex, invalidTypeCode, errorClass) && (!scoreRange || checkValidRange(CMIElement, value, scoreRange, invalidRangeCode, errorClass));
|
|
15518
15586
|
}
|
|
15519
15587
|
/**
|
|
@@ -15587,6 +15655,21 @@ class BaseAPI {
|
|
|
15587
15655
|
_offlineStorageService;
|
|
15588
15656
|
_cmiValueAccessService;
|
|
15589
15657
|
_courseId = "";
|
|
15658
|
+
_pendingCommitCount = 0;
|
|
15659
|
+
/**
|
|
15660
|
+
* Monotonic sequence for commits captured by this API instance. It is
|
|
15661
|
+
* intentionally not reset by reset().
|
|
15662
|
+
*/
|
|
15663
|
+
_commitSequence = 0;
|
|
15664
|
+
_commitSettleWaiters = [];
|
|
15665
|
+
/**
|
|
15666
|
+
* Canonical paths of every CMI element that has been explicitly assigned a
|
|
15667
|
+
* value via SetValue / loadFromJSON (both funnel through _commonSetCMIValue).
|
|
15668
|
+
* Used by standards that must tell "implemented but never set" apart from a
|
|
15669
|
+
* legitimately empty value when answering GetValue (SCORM 2004 error 403).
|
|
15670
|
+
* Cleared on reset so a fresh SCO attempt starts with nothing "set".
|
|
15671
|
+
*/
|
|
15672
|
+
_setCMIElements = /* @__PURE__ */ new Set();
|
|
15590
15673
|
/**
|
|
15591
15674
|
* Constructor for Base API class. Sets some shared API fields, as well as
|
|
15592
15675
|
* sets up options for the API.
|
|
@@ -15772,6 +15855,7 @@ class BaseAPI {
|
|
|
15772
15855
|
this.lastErrorCode = "0";
|
|
15773
15856
|
this._eventService.reset();
|
|
15774
15857
|
this.startingData = {};
|
|
15858
|
+
this._setCMIElements.clear();
|
|
15775
15859
|
if (this._offlineStorageService) {
|
|
15776
15860
|
this._offlineStorageService.updateSettings(this.settings);
|
|
15777
15861
|
if (settings?.courseId) {
|
|
@@ -15856,6 +15940,52 @@ class BaseAPI {
|
|
|
15856
15940
|
this._loggingService?.setLogHandler(settings.onLogMessage);
|
|
15857
15941
|
}
|
|
15858
15942
|
}
|
|
15943
|
+
/**
|
|
15944
|
+
* Gets the number of captured commit requests that have not yet settled.
|
|
15945
|
+
*
|
|
15946
|
+
* @return {number} The number of in-flight commits
|
|
15947
|
+
*/
|
|
15948
|
+
get pendingCommitCount() {
|
|
15949
|
+
return this._pendingCommitCount;
|
|
15950
|
+
}
|
|
15951
|
+
/**
|
|
15952
|
+
* Resolves when all currently in-flight commits have settled. A timeout is
|
|
15953
|
+
* best-effort: the promise resolves when it elapses even if commits remain,
|
|
15954
|
+
* and callers can inspect pendingCommitCount afterward to detect that case.
|
|
15955
|
+
*
|
|
15956
|
+
* @param {Object} [options] - Settle options
|
|
15957
|
+
* @param {number} [options.timeoutMs] - Maximum time to wait in milliseconds
|
|
15958
|
+
* @return {Promise<void>} A promise that resolves after the drain or timeout
|
|
15959
|
+
*/
|
|
15960
|
+
whenCommitsSettled(options) {
|
|
15961
|
+
if (this._pendingCommitCount === 0) {
|
|
15962
|
+
return Promise.resolve();
|
|
15963
|
+
}
|
|
15964
|
+
return new Promise((resolve) => {
|
|
15965
|
+
const waiter = { resolve };
|
|
15966
|
+
if (options?.timeoutMs !== void 0) {
|
|
15967
|
+
waiter.timeoutId = setTimeout(() => {
|
|
15968
|
+
const waiterIndex = this._commitSettleWaiters.indexOf(waiter);
|
|
15969
|
+
if (waiterIndex === -1) {
|
|
15970
|
+
return;
|
|
15971
|
+
}
|
|
15972
|
+
this._commitSettleWaiters.splice(waiterIndex, 1);
|
|
15973
|
+
resolve();
|
|
15974
|
+
}, options.timeoutMs);
|
|
15975
|
+
}
|
|
15976
|
+
this._commitSettleWaiters.push(waiter);
|
|
15977
|
+
});
|
|
15978
|
+
}
|
|
15979
|
+
/** Resolve and clear every waiter after the pending count reaches zero. */
|
|
15980
|
+
_flushCommitSettleWaiters() {
|
|
15981
|
+
const waiters = this._commitSettleWaiters.splice(0);
|
|
15982
|
+
for (const waiter of waiters) {
|
|
15983
|
+
if (waiter.timeoutId !== void 0) {
|
|
15984
|
+
clearTimeout(waiter.timeoutId);
|
|
15985
|
+
}
|
|
15986
|
+
waiter.resolve();
|
|
15987
|
+
}
|
|
15988
|
+
}
|
|
15859
15989
|
/**
|
|
15860
15990
|
* Terminates the current run of the API
|
|
15861
15991
|
* @param {string} callbackName
|
|
@@ -15876,7 +16006,7 @@ class BaseAPI {
|
|
|
15876
16006
|
} else {
|
|
15877
16007
|
stateCheckPassed = true;
|
|
15878
16008
|
this.processListeners("BeforeTerminate");
|
|
15879
|
-
const result = this.storeData(true);
|
|
16009
|
+
const result = this.storeData(true, "terminate");
|
|
15880
16010
|
if ((result.errorCode ?? 0) > 0) {
|
|
15881
16011
|
if (result.errorMessage) {
|
|
15882
16012
|
this.apiLog(
|
|
@@ -15928,6 +16058,9 @@ class BaseAPI {
|
|
|
15928
16058
|
} catch (e) {
|
|
15929
16059
|
returnValue = this.handleValueAccessException(CMIElement, e, returnValue);
|
|
15930
16060
|
}
|
|
16061
|
+
if (this.lastErrorCode === "0") {
|
|
16062
|
+
this.checkUninitializedGet(CMIElement, returnValue);
|
|
16063
|
+
}
|
|
15931
16064
|
this.processListeners(callbackName, CMIElement);
|
|
15932
16065
|
}
|
|
15933
16066
|
this.apiLog(callbackName, ": returned: " + returnValue, LogLevelEnum.INFO, CMIElement);
|
|
@@ -15937,6 +16070,10 @@ class BaseAPI {
|
|
|
15937
16070
|
if (this.lastErrorCode === "0") {
|
|
15938
16071
|
this.clearSCORMError(returnValue);
|
|
15939
16072
|
}
|
|
16073
|
+
const rawReturn = returnValue;
|
|
16074
|
+
if (typeof rawReturn === "number" || typeof rawReturn === "boolean") {
|
|
16075
|
+
return String(rawReturn);
|
|
16076
|
+
}
|
|
15940
16077
|
return returnValue;
|
|
15941
16078
|
}
|
|
15942
16079
|
/**
|
|
@@ -15989,9 +16126,10 @@ class BaseAPI {
|
|
|
15989
16126
|
* Orders LMS to store all content parameters
|
|
15990
16127
|
* @param {string} callbackName
|
|
15991
16128
|
* @param {boolean} checkTerminated
|
|
16129
|
+
* @param {CommitTrigger} trigger - What initiated the commit
|
|
15992
16130
|
* @return {string}
|
|
15993
16131
|
*/
|
|
15994
|
-
commit(callbackName, checkTerminated = false) {
|
|
16132
|
+
commit(callbackName, checkTerminated = false, trigger = "manual") {
|
|
15995
16133
|
this.clearScheduledCommit();
|
|
15996
16134
|
let returnValue = global_constants.SCORM_TRUE;
|
|
15997
16135
|
if (this.isNotInitialized()) {
|
|
@@ -16003,7 +16141,7 @@ class BaseAPI {
|
|
|
16003
16141
|
this.throwSCORMError("api", errorCode);
|
|
16004
16142
|
if (errorCode === 143) returnValue = global_constants.SCORM_FALSE;
|
|
16005
16143
|
} else {
|
|
16006
|
-
const result = this.storeData(false);
|
|
16144
|
+
const result = this.storeData(false, trigger);
|
|
16007
16145
|
const errorCode = result.errorCode ?? 0;
|
|
16008
16146
|
if (errorCode > 0) {
|
|
16009
16147
|
if (result.errorMessage) {
|
|
@@ -16234,7 +16372,16 @@ class BaseAPI {
|
|
|
16234
16372
|
* @return {string}
|
|
16235
16373
|
*/
|
|
16236
16374
|
_commonSetCMIValue(methodName, scorm2004, CMIElement, value) {
|
|
16237
|
-
|
|
16375
|
+
const result = this._cmiValueAccessService.setCMIValue(
|
|
16376
|
+
methodName,
|
|
16377
|
+
scorm2004,
|
|
16378
|
+
CMIElement,
|
|
16379
|
+
value
|
|
16380
|
+
);
|
|
16381
|
+
if (result === global_constants.SCORM_TRUE) {
|
|
16382
|
+
this._setCMIElements.add(CMIElement);
|
|
16383
|
+
}
|
|
16384
|
+
return result;
|
|
16238
16385
|
}
|
|
16239
16386
|
/**
|
|
16240
16387
|
* Gets a value from the CMI Object.
|
|
@@ -16248,6 +16395,18 @@ class BaseAPI {
|
|
|
16248
16395
|
_commonGetCMIValue(methodName, scorm2004, CMIElement) {
|
|
16249
16396
|
return this._cmiValueAccessService.getCMIValue(methodName, scorm2004, CMIElement);
|
|
16250
16397
|
}
|
|
16398
|
+
/**
|
|
16399
|
+
* Hook invoked by getValue after a successful resolution. Standards that must
|
|
16400
|
+
* distinguish "implemented but never set, no default value" from a
|
|
16401
|
+
* legitimately empty value override this to raise VALUE_NOT_INITIALIZED.
|
|
16402
|
+
* Default is a no-op, so SCORM 1.2 / AICC keep returning "" with code 0.
|
|
16403
|
+
*
|
|
16404
|
+
* @param {string} _CMIElement - the element that was read
|
|
16405
|
+
* @param {any} _returnValue - the value getCMIValue resolved
|
|
16406
|
+
* @protected
|
|
16407
|
+
*/
|
|
16408
|
+
checkUninitializedGet(_CMIElement, _returnValue) {
|
|
16409
|
+
}
|
|
16251
16410
|
/**
|
|
16252
16411
|
* Returns true if the API's current state is STATE_INITIALIZED
|
|
16253
16412
|
*
|
|
@@ -16330,9 +16489,14 @@ class BaseAPI {
|
|
|
16330
16489
|
* @param {string} functionName - The name of the function/event that occurred
|
|
16331
16490
|
* @param {string} CMIElement - Optional CMI element involved in the event
|
|
16332
16491
|
* @param {any} value - Optional value associated with the event
|
|
16492
|
+
* @param {CommitEventContext} context - Optional context for commit lifecycle events
|
|
16333
16493
|
*/
|
|
16334
|
-
processListeners(functionName, CMIElement, value) {
|
|
16335
|
-
|
|
16494
|
+
processListeners(functionName, CMIElement, value, context) {
|
|
16495
|
+
if (context !== void 0) {
|
|
16496
|
+
this._eventService.processListeners(functionName, CMIElement, value, context);
|
|
16497
|
+
} else {
|
|
16498
|
+
this._eventService.processListeners(functionName, CMIElement, value);
|
|
16499
|
+
}
|
|
16336
16500
|
}
|
|
16337
16501
|
/**
|
|
16338
16502
|
* Throws a SCORM error with the specified error number and optional message.
|
|
@@ -16465,36 +16629,106 @@ class BaseAPI {
|
|
|
16465
16629
|
* @param {string} url - The URL to send the request to
|
|
16466
16630
|
* @param {CommitObject | StringKeyMap | Array<any>} params - The parameters to send
|
|
16467
16631
|
* @param {boolean} immediate - Whether to send the request immediately without waiting
|
|
16632
|
+
* @param {CommitTrigger} [trigger] - What initiated the commit
|
|
16468
16633
|
* @returns {ResultObject} - The result of the request
|
|
16469
16634
|
*/
|
|
16470
|
-
processHttpRequest(url, params, immediate = false) {
|
|
16471
|
-
|
|
16472
|
-
|
|
16473
|
-
|
|
16474
|
-
|
|
16475
|
-
|
|
16476
|
-
)
|
|
16477
|
-
|
|
16478
|
-
|
|
16479
|
-
|
|
16635
|
+
processHttpRequest(url, params, immediate = false, trigger) {
|
|
16636
|
+
const sequence = ++this._commitSequence;
|
|
16637
|
+
this._pendingCommitCount += 1;
|
|
16638
|
+
let settled = false;
|
|
16639
|
+
let completionDeferred = false;
|
|
16640
|
+
const settle = () => {
|
|
16641
|
+
if (settled) {
|
|
16642
|
+
return;
|
|
16643
|
+
}
|
|
16644
|
+
settled = true;
|
|
16645
|
+
this._pendingCommitCount -= 1;
|
|
16646
|
+
if (this._pendingCommitCount === 0) {
|
|
16647
|
+
this._flushCommitSettleWaiters();
|
|
16648
|
+
}
|
|
16649
|
+
};
|
|
16650
|
+
try {
|
|
16651
|
+
const resolvedTrigger = trigger ?? (immediate ? "terminate" : "manual");
|
|
16652
|
+
let finalParams = params;
|
|
16653
|
+
if (immediate && this.settings.terminateCommitPayloadField) {
|
|
16654
|
+
const field = this.settings.terminateCommitPayloadField;
|
|
16655
|
+
if (Array.isArray(finalParams)) {
|
|
16656
|
+
finalParams = [...finalParams, `${encodeURIComponent(field)}=true`];
|
|
16657
|
+
} else if (finalParams && typeof finalParams === "object") {
|
|
16658
|
+
finalParams = { ...finalParams, [field]: true };
|
|
16659
|
+
}
|
|
16660
|
+
}
|
|
16661
|
+
if (this.settings.includeCommitSequence === true) {
|
|
16662
|
+
if (Array.isArray(finalParams)) {
|
|
16663
|
+
finalParams = [...finalParams, `commitSequence=${sequence}`];
|
|
16664
|
+
} else if (finalParams && typeof finalParams === "object") {
|
|
16665
|
+
finalParams = { ...finalParams, commitSequence: sequence };
|
|
16666
|
+
}
|
|
16667
|
+
}
|
|
16668
|
+
const finalUrl = immediate && this.settings.terminateCommitParam ? appendQueryParam(url, this.settings.terminateCommitParam, "true") : url;
|
|
16669
|
+
const metadata = {
|
|
16670
|
+
isTerminateCommit: immediate,
|
|
16671
|
+
trigger: resolvedTrigger,
|
|
16672
|
+
sequence
|
|
16673
|
+
};
|
|
16674
|
+
const context = {
|
|
16675
|
+
url: finalUrl,
|
|
16676
|
+
trigger: resolvedTrigger,
|
|
16677
|
+
isTerminateCommit: immediate,
|
|
16678
|
+
sequence
|
|
16679
|
+
};
|
|
16680
|
+
if (this.settings.enableOfflineSupport && this._offlineStorageService && !this._offlineStorageService.isDeviceOnline() && this._courseId) {
|
|
16480
16681
|
this.apiLog(
|
|
16481
16682
|
"processHttpRequest",
|
|
16482
|
-
"
|
|
16483
|
-
LogLevelEnum.
|
|
16683
|
+
"Device is offline, storing data locally",
|
|
16684
|
+
LogLevelEnum.INFO
|
|
16484
16685
|
);
|
|
16485
|
-
|
|
16486
|
-
|
|
16487
|
-
|
|
16488
|
-
|
|
16686
|
+
if (finalParams && typeof finalParams === "object" && "cmi" in finalParams) {
|
|
16687
|
+
return this._offlineStorageService.storeOffline(
|
|
16688
|
+
this._courseId,
|
|
16689
|
+
finalParams,
|
|
16690
|
+
{ isTerminateCommit: immediate, sequence }
|
|
16691
|
+
);
|
|
16692
|
+
} else {
|
|
16693
|
+
this.apiLog(
|
|
16694
|
+
"processHttpRequest",
|
|
16695
|
+
"Invalid commit data format for offline storage",
|
|
16696
|
+
LogLevelEnum.ERROR
|
|
16697
|
+
);
|
|
16698
|
+
return {
|
|
16699
|
+
result: global_constants.SCORM_FALSE,
|
|
16700
|
+
errorCode: this._error_codes.GENERAL ?? 101
|
|
16701
|
+
};
|
|
16702
|
+
}
|
|
16703
|
+
}
|
|
16704
|
+
const apiLog = (functionName, message, level, element) => this.apiLog(functionName, message, level, element);
|
|
16705
|
+
const processListeners = (functionName, CMIElement, value) => {
|
|
16706
|
+
if (functionName === "CommitSuccess" || functionName === "CommitError") {
|
|
16707
|
+
if (functionName === "CommitError" && typeof value === "number") {
|
|
16708
|
+
context.errorCode = value;
|
|
16709
|
+
}
|
|
16710
|
+
settle();
|
|
16711
|
+
this.processListeners(functionName, CMIElement, value, context);
|
|
16712
|
+
} else {
|
|
16713
|
+
this.processListeners(functionName, CMIElement, value);
|
|
16714
|
+
}
|
|
16715
|
+
};
|
|
16716
|
+
const result = this._httpService.processHttpRequest(
|
|
16717
|
+
finalUrl,
|
|
16718
|
+
finalParams,
|
|
16719
|
+
immediate,
|
|
16720
|
+
apiLog,
|
|
16721
|
+
processListeners,
|
|
16722
|
+
metadata,
|
|
16723
|
+
settle
|
|
16724
|
+
);
|
|
16725
|
+
completionDeferred = this._httpService.reportsRequestCompletion === true;
|
|
16726
|
+
return result;
|
|
16727
|
+
} finally {
|
|
16728
|
+
if (!completionDeferred) {
|
|
16729
|
+
settle();
|
|
16489
16730
|
}
|
|
16490
16731
|
}
|
|
16491
|
-
return this._httpService.processHttpRequest(
|
|
16492
|
-
url,
|
|
16493
|
-
params,
|
|
16494
|
-
immediate,
|
|
16495
|
-
(functionName, message, level, element) => this.apiLog(functionName, message, level, element),
|
|
16496
|
-
(functionName, CMIElement, value) => this.processListeners(functionName, CMIElement, value)
|
|
16497
|
-
);
|
|
16498
16732
|
}
|
|
16499
16733
|
/**
|
|
16500
16734
|
* Schedules a commit operation to occur after a specified delay.
|
|
@@ -16654,6 +16888,11 @@ class CMIScore extends BaseCMI {
|
|
|
16654
16888
|
__invalid_range_code;
|
|
16655
16889
|
__decimal_regex;
|
|
16656
16890
|
__error_class;
|
|
16891
|
+
/**
|
|
16892
|
+
* When true, an empty string is a valid value (clears the element). SCORM 1.2
|
|
16893
|
+
* score elements may be blank per the ADL 1.2 CTS; SCORM 2004 leaves this off.
|
|
16894
|
+
*/
|
|
16895
|
+
__allow_empty_string;
|
|
16657
16896
|
_raw = "";
|
|
16658
16897
|
_min = "";
|
|
16659
16898
|
_max;
|
|
@@ -16688,6 +16927,7 @@ class CMIScore extends BaseCMI {
|
|
|
16688
16927
|
this.__invalid_range_code = params.invalidRangeCode || scorm12_errors.VALUE_OUT_OF_RANGE;
|
|
16689
16928
|
this.__decimal_regex = params.decimalRegex || scorm12_regex.CMIDecimal;
|
|
16690
16929
|
this.__error_class = params.errorClass;
|
|
16930
|
+
this.__allow_empty_string = params.allowEmptyString ?? false;
|
|
16691
16931
|
}
|
|
16692
16932
|
/**
|
|
16693
16933
|
* Called when the API has been reset
|
|
@@ -16734,7 +16974,8 @@ class CMIScore extends BaseCMI {
|
|
|
16734
16974
|
this.__score_range,
|
|
16735
16975
|
this.__invalid_type_code,
|
|
16736
16976
|
this.__invalid_range_code,
|
|
16737
|
-
this.__error_class
|
|
16977
|
+
this.__error_class,
|
|
16978
|
+
this.__allow_empty_string
|
|
16738
16979
|
)) {
|
|
16739
16980
|
this._raw = raw;
|
|
16740
16981
|
}
|
|
@@ -16758,7 +16999,8 @@ class CMIScore extends BaseCMI {
|
|
|
16758
16999
|
this.__score_range,
|
|
16759
17000
|
this.__invalid_type_code,
|
|
16760
17001
|
this.__invalid_range_code,
|
|
16761
|
-
this.__error_class
|
|
17002
|
+
this.__error_class,
|
|
17003
|
+
this.__allow_empty_string
|
|
16762
17004
|
)) {
|
|
16763
17005
|
this._min = min;
|
|
16764
17006
|
}
|
|
@@ -16782,7 +17024,8 @@ class CMIScore extends BaseCMI {
|
|
|
16782
17024
|
this.__score_range,
|
|
16783
17025
|
this.__invalid_type_code,
|
|
16784
17026
|
this.__invalid_range_code,
|
|
16785
|
-
this.__error_class
|
|
17027
|
+
this.__error_class,
|
|
17028
|
+
this.__allow_empty_string
|
|
16786
17029
|
)) {
|
|
16787
17030
|
this._max = max;
|
|
16788
17031
|
}
|
|
@@ -16839,7 +17082,9 @@ class CMICore extends BaseCMI {
|
|
|
16839
17082
|
invalidErrorCode: scorm12_errors.INVALID_SET_VALUE,
|
|
16840
17083
|
invalidTypeCode: scorm12_errors.TYPE_MISMATCH,
|
|
16841
17084
|
invalidRangeCode: scorm12_errors.VALUE_OUT_OF_RANGE,
|
|
16842
|
-
errorClass: Scorm12ValidationError
|
|
17085
|
+
errorClass: Scorm12ValidationError,
|
|
17086
|
+
// SCORM 1.2 score elements may be set blank (clears the value), per ADL 1.2 CTS.
|
|
17087
|
+
allowEmptyString: true
|
|
16843
17088
|
});
|
|
16844
17089
|
}
|
|
16845
17090
|
score;
|
|
@@ -17269,7 +17514,9 @@ let CMIObjectivesObject$1 = class CMIObjectivesObject extends BaseCMI {
|
|
|
17269
17514
|
invalidErrorCode: scorm12_errors.INVALID_SET_VALUE,
|
|
17270
17515
|
invalidTypeCode: scorm12_errors.TYPE_MISMATCH,
|
|
17271
17516
|
invalidRangeCode: scorm12_errors.VALUE_OUT_OF_RANGE,
|
|
17272
|
-
errorClass: Scorm12ValidationError
|
|
17517
|
+
errorClass: Scorm12ValidationError,
|
|
17518
|
+
// SCORM 1.2 score elements may be set blank (clears the value), per ADL 1.2 CTS.
|
|
17519
|
+
allowEmptyString: true
|
|
17273
17520
|
});
|
|
17274
17521
|
}
|
|
17275
17522
|
score;
|
|
@@ -18739,16 +18986,17 @@ class Scorm12API extends BaseAPI {
|
|
|
18739
18986
|
* Attempts to store the data to the LMS
|
|
18740
18987
|
*
|
|
18741
18988
|
* @param {boolean} terminateCommit
|
|
18989
|
+
* @param {CommitTrigger} [trigger] - What initiated the commit
|
|
18742
18990
|
* @return {ResultObject}
|
|
18743
18991
|
*/
|
|
18744
|
-
storeData(terminateCommit) {
|
|
18992
|
+
storeData(terminateCommit, trigger) {
|
|
18745
18993
|
if (terminateCommit) {
|
|
18746
18994
|
const originalStatus = this.cmi.core.lesson_status;
|
|
18747
18995
|
if (this.cmi.core.lesson_mode === "browse") {
|
|
18748
18996
|
const startingStatus = this.startingData?.cmi?.core?.lesson_status || "";
|
|
18749
18997
|
if (startingStatus === "" && originalStatus === "not attempted") {
|
|
18750
18998
|
this.cmi.core.lesson_status = "browsed";
|
|
18751
|
-
return this.processCommitData(terminateCommit);
|
|
18999
|
+
return this.processCommitData(terminateCommit, trigger);
|
|
18752
19000
|
}
|
|
18753
19001
|
}
|
|
18754
19002
|
if (!this.cmi.core.lesson_status || !this.statusSetByModule && this.cmi.core.lesson_status === "not attempted") {
|
|
@@ -18777,12 +19025,17 @@ class Scorm12API extends BaseAPI {
|
|
|
18777
19025
|
}
|
|
18778
19026
|
}
|
|
18779
19027
|
}
|
|
18780
|
-
return this.processCommitData(terminateCommit);
|
|
19028
|
+
return this.processCommitData(terminateCommit, trigger);
|
|
18781
19029
|
}
|
|
18782
|
-
processCommitData(terminateCommit) {
|
|
19030
|
+
processCommitData(terminateCommit, trigger) {
|
|
18783
19031
|
const commitObject = this.getCommitObject(terminateCommit);
|
|
18784
19032
|
if (typeof this.settings.lmsCommitUrl === "string") {
|
|
18785
|
-
return this.processHttpRequest(
|
|
19033
|
+
return this.processHttpRequest(
|
|
19034
|
+
this.settings.lmsCommitUrl,
|
|
19035
|
+
commitObject,
|
|
19036
|
+
terminateCommit,
|
|
19037
|
+
trigger
|
|
19038
|
+
);
|
|
18786
19039
|
} else {
|
|
18787
19040
|
return {
|
|
18788
19041
|
result: global_constants.SCORM_TRUE,
|
|
@@ -24366,6 +24619,35 @@ class Scorm2004DataSerializer {
|
|
|
24366
24619
|
}
|
|
24367
24620
|
}
|
|
24368
24621
|
|
|
24622
|
+
const NO_DEFAULT_2004_ELEMENTS = /* @__PURE__ */ new Set([
|
|
24623
|
+
"cmi.suspend_data",
|
|
24624
|
+
"cmi.location",
|
|
24625
|
+
"cmi.scaled_passing_score",
|
|
24626
|
+
"cmi.max_time_allowed",
|
|
24627
|
+
"cmi.completion_threshold",
|
|
24628
|
+
"cmi.progress_measure",
|
|
24629
|
+
"cmi.score.scaled",
|
|
24630
|
+
"cmi.score.raw",
|
|
24631
|
+
"cmi.score.min",
|
|
24632
|
+
"cmi.score.max",
|
|
24633
|
+
"cmi.objectives.N.score.scaled",
|
|
24634
|
+
"cmi.objectives.N.score.raw",
|
|
24635
|
+
"cmi.objectives.N.score.min",
|
|
24636
|
+
"cmi.objectives.N.score.max",
|
|
24637
|
+
"cmi.objectives.N.progress_measure",
|
|
24638
|
+
"cmi.objectives.N.description",
|
|
24639
|
+
"cmi.interactions.N.weighting",
|
|
24640
|
+
"cmi.interactions.N.type",
|
|
24641
|
+
"cmi.interactions.N.timestamp",
|
|
24642
|
+
"cmi.interactions.N.result",
|
|
24643
|
+
"cmi.interactions.N.latency",
|
|
24644
|
+
"cmi.interactions.N.learner_response",
|
|
24645
|
+
"cmi.interactions.N.description",
|
|
24646
|
+
"cmi.comments_from_learner.N.timestamp"
|
|
24647
|
+
]);
|
|
24648
|
+
function normalizeCMIIndices(CMIElement) {
|
|
24649
|
+
return CMIElement.replace(/\.\d+(?=\.|$)/g, ".N");
|
|
24650
|
+
}
|
|
24369
24651
|
class Scorm2004API extends BaseAPI {
|
|
24370
24652
|
_version = "1.0";
|
|
24371
24653
|
_sequencing;
|
|
@@ -24891,6 +25173,39 @@ class Scorm2004API extends BaseAPI {
|
|
|
24891
25173
|
getCMIValue(CMIElement) {
|
|
24892
25174
|
return this._commonGetCMIValue("GetValue", true, CMIElement);
|
|
24893
25175
|
}
|
|
25176
|
+
/**
|
|
25177
|
+
* Raises 403 (VALUE_NOT_INITIALIZED) when an implemented, no-default element is
|
|
25178
|
+
* read before it has ever been set, per IEEE 1484.11.2 / SCORM 2004 RTE.
|
|
25179
|
+
*
|
|
25180
|
+
* Invoked by BaseAPI.getValue after an otherwise-successful resolution, so it
|
|
25181
|
+
* only sees values the data model already returned cleanly. The decision is
|
|
25182
|
+
* deliberately gated to this public boundary (never the getters) to keep
|
|
25183
|
+
* serialization/commit/rollup — which read the getters directly — unaffected.
|
|
25184
|
+
*
|
|
25185
|
+
* A 403 is raised only when all hold:
|
|
25186
|
+
* - the resolved value is "" — any non-empty value is by definition set,
|
|
25187
|
+
* including values written by internal sequencing/activity-tree paths that
|
|
25188
|
+
* bypass _commonSetCMIValue (those only ever write non-empty values);
|
|
25189
|
+
* - the element was never set — _setCMIElements records every successful
|
|
25190
|
+
* SetValue, loadFromJSON, and global-objective restore (all route through
|
|
25191
|
+
* _commonSetCMIValue), so an explicit SetValue(x, "") still reads back as
|
|
25192
|
+
* "" with code 0; and
|
|
25193
|
+
* - the element has no spec-defined default ({@link NO_DEFAULT_2004_ELEMENTS}).
|
|
25194
|
+
*
|
|
25195
|
+
* @param {string} CMIElement
|
|
25196
|
+
* @param {*} returnValue - the value getCMIValue resolved
|
|
25197
|
+
* @protected
|
|
25198
|
+
*/
|
|
25199
|
+
checkUninitializedGet(CMIElement, returnValue) {
|
|
25200
|
+
if (returnValue !== "") return;
|
|
25201
|
+
if (this._setCMIElements.has(CMIElement)) return;
|
|
25202
|
+
if (!NO_DEFAULT_2004_ELEMENTS.has(normalizeCMIIndices(CMIElement))) return;
|
|
25203
|
+
this.throwSCORMError(
|
|
25204
|
+
CMIElement,
|
|
25205
|
+
this._error_codes.VALUE_NOT_INITIALIZED ?? 403,
|
|
25206
|
+
`The data model element passed to GetValue (${CMIElement}) has not been initialized.`
|
|
25207
|
+
);
|
|
25208
|
+
}
|
|
24894
25209
|
/**
|
|
24895
25210
|
* Returns the message that corresponds to errorNumber.
|
|
24896
25211
|
* @param {(string|number)} errorNumber
|
|
@@ -24937,9 +25252,10 @@ class Scorm2004API extends BaseAPI {
|
|
|
24937
25252
|
/**
|
|
24938
25253
|
* Attempts to store the data to the LMS
|
|
24939
25254
|
* @param {boolean} terminateCommit
|
|
25255
|
+
* @param {CommitTrigger} [trigger] - What initiated the commit
|
|
24940
25256
|
* @return {ResultObject}
|
|
24941
25257
|
*/
|
|
24942
|
-
storeData(terminateCommit) {
|
|
25258
|
+
storeData(terminateCommit, trigger) {
|
|
24943
25259
|
if (terminateCommit) {
|
|
24944
25260
|
if (this.cmi.mode === "normal") {
|
|
24945
25261
|
if (this.cmi.credit === "credit") {
|
|
@@ -24987,7 +25303,8 @@ class Scorm2004API extends BaseAPI {
|
|
|
24987
25303
|
const result = this.processHttpRequest(
|
|
24988
25304
|
this.settings.lmsCommitUrl,
|
|
24989
25305
|
commitObject,
|
|
24990
|
-
terminateCommit
|
|
25306
|
+
terminateCommit,
|
|
25307
|
+
trigger
|
|
24991
25308
|
);
|
|
24992
25309
|
if (navRequest && result.navRequest !== void 0 && result.navRequest !== "" && typeof result.navRequest === "string") {
|
|
24993
25310
|
const parsed = parseNavigationRequest(result.navRequest);
|