scorm-again
Version:
A modern SCORM JavaScript run-time library for SCORM 1.2 and SCORM 2004
1,382 lines (1,369 loc) • 203 kB
JavaScript
const SECONDS_PER_MINUTE = 60;
const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
const getSecondsAsHHMMSS = (totalSeconds) => {
if (!totalSeconds || totalSeconds <= 0) {
return "00:00:00";
}
const hours = Math.floor(totalSeconds / SECONDS_PER_HOUR);
const dateObj = new Date(totalSeconds * 1e3);
const minutes = dateObj.getUTCMinutes();
const seconds = dateObj.getSeconds();
const ms = totalSeconds % 1;
let msStr = "";
if (countDecimals(ms) > 0) {
if (countDecimals(ms) > 2) {
msStr = ms.toFixed(2);
} else {
msStr = String(ms);
}
msStr = "." + msStr.split(".")[1];
}
return (hours + ":" + minutes + ":" + seconds).replace(/\b\d\b/g, "0$&") + msStr;
};
const getTimeAsSeconds = memoize(
(timeString, timeRegex) => {
if (typeof timeString === "number" || typeof timeString === "boolean") {
timeString = String(timeString);
}
if (typeof timeRegex === "string") {
timeRegex = new RegExp(timeRegex);
}
if (!timeString) {
return 0;
}
if (!timeString.match(timeRegex)) {
if (/^\d+(?:\.\d+)?$/.test(timeString)) {
return Number(timeString);
}
return 0;
}
const parts = timeString.split(":");
const hours = Number(parts[0]);
const minutes = Number(parts[1]);
const seconds = Number(parts[2]);
return hours * 3600 + minutes * 60 + seconds;
},
// Custom key function to handle RegExp objects which can't be stringified
(timeString, timeRegex) => {
const timeStr = typeof timeString === "string" ? timeString : String(timeString ?? "");
const regexStr = typeof timeRegex === "string" ? timeRegex : timeRegex?.toString() ?? "";
return `${timeStr}:${regexStr}`;
}
);
const getDurationAsSeconds = memoize(
(duration, durationRegex) => {
if (typeof durationRegex === "string") {
durationRegex = new RegExp(durationRegex);
}
if (!duration || !duration?.match?.(durationRegex)) {
return 0;
}
const [, years, months, weeks, days, hours, minutes, seconds] = new RegExp(durationRegex).exec?.(duration) ?? [];
let result = 0;
result += Number(seconds) || 0;
result += Number(minutes) * 60 || 0;
result += Number(hours) * 3600 || 0;
result += Number(days) * (60 * 60 * 24) || 0;
result += Number(weeks) * (60 * 60 * 24 * 7) || 0;
result += Number(months) * (60 * 60 * 24 * 30) || 0;
result += Number(years) * (60 * 60 * 24 * 365) || 0;
return result;
},
// Custom key function to handle RegExp objects which can't be stringified
(duration, durationRegex) => {
const durationStr = duration ?? "";
const regexStr = typeof durationRegex === "string" ? durationRegex : durationRegex?.toString() ?? "";
return `${durationStr}:${regexStr}`;
}
);
function addHHMMSSTimeStrings(first, second, timeRegex) {
if (typeof timeRegex === "string") {
timeRegex = new RegExp(timeRegex);
}
return getSecondsAsHHMMSS(
getTimeAsSeconds(first, timeRegex) + getTimeAsSeconds(second, timeRegex)
);
}
function flatten(data) {
const result = {};
function recurse(cur, prop) {
if (Object(cur) !== cur) {
result[prop] = cur;
} else if (Array.isArray(cur)) {
cur.forEach((item, i) => {
recurse(item, `${prop}[${i}]`);
});
if (cur.length === 0) result[prop] = [];
} else {
const keys = Object.keys(cur).filter((p) => Object.prototype.hasOwnProperty.call(cur, p));
const isEmpty = keys.length === 0;
keys.forEach((p) => {
recurse(cur[p], prop ? `${prop}.${p}` : p);
});
if (isEmpty && prop) result[prop] = {};
}
}
recurse(data, "");
return result;
}
function unflatten(data) {
if (Object(data) !== data || Array.isArray(data)) return data;
const result = {};
const pattern = /\.?([^.[\]]+)|\[(\d+)]/g;
Object.keys(data).filter((p) => Object.prototype.hasOwnProperty.call(data, p)).forEach((p) => {
let cur = result;
let prop = "";
const regex = new RegExp(pattern);
Array.from(
{ length: p.match(new RegExp(pattern, "g"))?.length ?? 0 },
() => regex.exec(p)
).forEach((m) => {
if (m) {
cur = cur[prop] ?? (cur[prop] = m[2] ? [] : {});
prop = m[2] || m[1] || "";
}
});
cur[prop] = data[p];
});
return result[""] ?? result;
}
function countDecimals(num) {
if (Math.floor(num) === num || String(num)?.indexOf?.(".") < 0) return 0;
const parts = num.toString().split(".")?.[1];
return parts?.length ?? 0;
}
function formatMessage(functionName, message, CMIElement) {
const baseLength = 20;
let messageString = functionName ? `${String(functionName).padEnd(baseLength)}: ` : "";
if (CMIElement) {
const CMIElementBaseLength = 70;
messageString += CMIElement;
messageString = messageString.padEnd(CMIElementBaseLength);
}
messageString += message ?? "";
return messageString;
}
function stringMatches(str, tester) {
if (typeof str !== "string") {
return false;
}
return new RegExp(tester).test(str);
}
function memoize(fn, keyFn) {
const cache = /* @__PURE__ */ new Map();
return ((...args) => {
const key = keyFn ? keyFn(...args) : JSON.stringify(args);
return cache.has(key) ? cache.get(key) : (() => {
const result = fn(...args);
cache.set(key, result);
return result;
})();
});
}
class BaseCMI {
/**
* Flag used during JSON serialization to allow getter access without initialization checks.
* When true, getters can be accessed before the API is initialized, which is necessary
* for serializing the CMI data structure to JSON format.
*/
jsonString = false;
_cmi_element;
_initialized = false;
/**
* Constructor for BaseCMI
* @param {string} cmi_element
*/
constructor(cmi_element) {
this._cmi_element = cmi_element;
}
/**
* Getter for _initialized
* @return {boolean}
*/
get initialized() {
return this._initialized;
}
/**
* Called when the API has been initialized after the CMI has been created
*/
initialize() {
this._initialized = true;
}
}
class BaseRootCMI extends BaseCMI {
_start_time;
/**
* Start time of the session
* @type {number | undefined}
* @protected
*/
get start_time() {
return this._start_time;
}
/**
* Setter for start_time. Can only be called once.
*/
setStartTime() {
if (this._start_time === void 0) {
this._start_time = (/* @__PURE__ */ new Date()).getTime();
} else {
throw new Error("Start time has already been set.");
}
}
}
class BaseScormValidationError extends Error {
constructor(CMIElement, errorCode) {
super(`${CMIElement} : ${errorCode.toString()}`);
this._errorCode = errorCode;
Object.setPrototypeOf(this, BaseScormValidationError.prototype);
}
_errorCode;
/**
* Getter for _errorCode
* @return {number}
*/
get errorCode() {
return this._errorCode;
}
}
class ValidationError extends BaseScormValidationError {
/**
* Constructor to take in an error message and code
* @param {string} CMIElement
* @param {number} errorCode
* @param {string} errorMessage
* @param {string} detailedMessage
*/
constructor(CMIElement, errorCode, errorMessage, detailedMessage) {
super(CMIElement, errorCode);
this.message = `${CMIElement} : ${errorMessage}`;
this._errorMessage = errorMessage;
if (detailedMessage) {
this._detailedMessage = detailedMessage;
}
Object.setPrototypeOf(this, ValidationError.prototype);
}
_errorMessage;
_detailedMessage = "";
/**
* Getter for _errorMessage
* @return {string}
*/
get errorMessage() {
return this._errorMessage;
}
/**
* Getter for _detailedMessage
* @return {string}
*/
get detailedMessage() {
return this._detailedMessage;
}
}
const global_constants = {
SCORM_TRUE: "true",
SCORM_FALSE: "false",
STATE_NOT_INITIALIZED: 0,
STATE_INITIALIZED: 1,
STATE_TERMINATED: 2
};
const scorm12_constants = {
// Children lists
cmi_children: "core,suspend_data,launch_data,comments,objectives,student_data,student_preference,interactions",
core_children: "student_id,student_name,lesson_location,credit,lesson_status,entry,score,total_time,lesson_mode,exit,session_time",
score_children: "raw,min,max",
objectives_children: "id,score,status",
correct_responses_children: "pattern",
student_data_children: "mastery_score,max_time_allowed,time_limit_action",
student_preference_children: "audio,language,speed,text",
interactions_children: "id,objectives,time,type,correct_responses,weighting,student_response,result,latency",
error_descriptions: {
"0": {
basicMessage: "No Error",
detailMessage: "No error occurred, the previous API call was successful."
},
"101": {
basicMessage: "General Exception",
detailMessage: "No specific error code exists to describe the error."
},
"201": {
basicMessage: "Invalid argument error",
detailMessage: "Indicates that an argument represents an invalid data model element or is otherwise incorrect."
},
"202": {
basicMessage: "Element cannot have children",
detailMessage: 'Indicates that LMSGetValue was called with a data model element name that ends in "_children" for a data model element that does not support the "_children" suffix.'
},
"203": {
basicMessage: "Element not an array - cannot have count",
detailMessage: 'Indicates that LMSGetValue was called with a data model element name that ends in "_count" for a data model element that does not support the "_count" suffix.'
},
"301": {
basicMessage: "Not initialized",
detailMessage: "Indicates that an API call was made before the call to lmsInitialize."
},
"401": {
basicMessage: "Not implemented error",
detailMessage: "The data model element indicated in a call to LMSGetValue or LMSSetValue is valid, but was not implemented by this LMS. SCORM 1.2 defines a set of data model elements as being optional for an LMS to implement."
},
"402": {
basicMessage: "Invalid set value, element is a keyword",
detailMessage: 'Indicates that LMSSetValue was called on a data model element that represents a keyword (elements that end in "_children" and "_count").'
},
"403": {
basicMessage: "Element is read only",
detailMessage: "LMSSetValue was called with a data model element that can only be read."
},
"404": {
basicMessage: "Element is write only",
detailMessage: "LMSGetValue was called on a data model element that can only be written to."
},
"405": {
basicMessage: "Incorrect Data Type",
detailMessage: "LMSSetValue was called with a value that is not consistent with the data format of the supplied data model element."
},
"407": {
basicMessage: "Element Value Out Of Range",
detailMessage: "The numeric value supplied to a LMSSetValue call is outside of the numeric range allowed for the supplied data model element."
},
"408": {
basicMessage: "Data Model Dependency Not Established",
detailMessage: "Some data model elements cannot be set until another data model element was set. This error condition indicates that the prerequisite element was not set before the dependent element."
}
}
};
const scorm12_errors$1 = scorm12_constants.error_descriptions;
class Scorm12ValidationError extends ValidationError {
/**
* Constructor to take in an error code
* @param {string} CMIElement
* @param {number} errorCode
*/
constructor(CMIElement, errorCode) {
if ({}.hasOwnProperty.call(scorm12_errors$1, String(errorCode))) {
super(
CMIElement,
errorCode,
scorm12_errors$1[String(errorCode)]?.basicMessage || "Unknown error",
scorm12_errors$1[String(errorCode)]?.detailMessage
);
} else {
super(
CMIElement,
101,
scorm12_errors$1["101"]?.basicMessage,
scorm12_errors$1["101"]?.detailMessage
);
}
Object.setPrototypeOf(this, Scorm12ValidationError.prototype);
}
}
const global_errors = {
GENERAL: 101,
INITIALIZATION_FAILED: 101,
INITIALIZED: 101,
TERMINATED: 101,
TERMINATION_FAILURE: 101,
TERMINATION_BEFORE_INIT: 101,
MULTIPLE_TERMINATION: 101,
RETRIEVE_BEFORE_INIT: 101,
RETRIEVE_AFTER_TERM: 101,
STORE_BEFORE_INIT: 101,
STORE_AFTER_TERM: 101,
COMMIT_BEFORE_INIT: 101,
COMMIT_AFTER_TERM: 101,
ARGUMENT_ERROR: 101,
CHILDREN_ERROR: 101,
COUNT_ERROR: 101,
GENERAL_GET_FAILURE: 101,
GENERAL_SET_FAILURE: 101,
GENERAL_COMMIT_FAILURE: 101,
UNDEFINED_DATA_MODEL: 101,
UNIMPLEMENTED_ELEMENT: 101,
VALUE_NOT_INITIALIZED: 101,
INVALID_SET_VALUE: 101,
READ_ONLY_ELEMENT: 101,
WRITE_ONLY_ELEMENT: 101,
TYPE_MISMATCH: 101,
VALUE_OUT_OF_RANGE: 101,
DEPENDENCY_NOT_ESTABLISHED: 101
};
const scorm12_errors = {
...global_errors,
RETRIEVE_BEFORE_INIT: 301,
STORE_BEFORE_INIT: 301,
COMMIT_BEFORE_INIT: 301,
ARGUMENT_ERROR: 201,
CHILDREN_ERROR: 202,
COUNT_ERROR: 203,
UNDEFINED_DATA_MODEL: 401,
UNIMPLEMENTED_ELEMENT: 401,
VALUE_NOT_INITIALIZED: 301,
INVALID_SET_VALUE: 402,
READ_ONLY_ELEMENT: 403,
WRITE_ONLY_ELEMENT: 404,
TYPE_MISMATCH: 405,
VALUE_OUT_OF_RANGE: 407,
DEPENDENCY_NOT_ESTABLISHED: 408
};
class CMIArray extends BaseCMI {
_errorCode;
_errorClass;
__children;
childArray;
/**
* Constructor cmi *.n arrays
* @param {object} params
*/
constructor(params) {
super(params.CMIElement);
this.__children = params.children;
this._errorCode = params.errorCode ?? scorm12_errors.GENERAL;
this._errorClass = params.errorClass || BaseScormValidationError;
this.childArray = [];
}
/**
* Called when the API has been reset
*/
reset(wipe = false) {
this._initialized = false;
if (wipe) {
this.childArray = [];
} else {
for (let i = 0; i < this.childArray.length; i++) {
this.childArray[i]?.reset();
}
}
}
/**
* Getter for _children
* @return {string}
*/
get _children() {
return this.__children;
}
/**
* Setter for _children. Just throws an error.
* @param {string} _children
*/
set _children(_children) {
throw new this._errorClass(this._cmi_element + "._children", this._errorCode);
}
/**
* Getter for _count
* @return {number}
*/
get _count() {
return this.childArray.length;
}
/**
* Setter for _count. Just throws an error.
* @param {number} _count
*/
set _count(_count) {
throw new this._errorClass(this._cmi_element + "._count", this._errorCode);
}
/**
* toJSON for *.n arrays
* @return {object}
*/
toJSON() {
this.jsonString = true;
const result = {};
for (let i = 0; i < this.childArray.length; i++) {
result[i + ""] = this.childArray[i];
}
this.jsonString = false;
return result;
}
}
const SuccessStatus = {
PASSED: "passed",
FAILED: "failed",
UNKNOWN: "unknown"
};
const CompletionStatus = {
COMPLETED: "completed",
INCOMPLETE: "incomplete",
UNKNOWN: "unknown"
};
const LogLevelEnum = {
_: 0,
DEBUG: 1,
INFO: 2,
WARN: 3,
ERROR: 4,
NONE: 5
};
const DefaultSettings = {
autocommit: false,
autocommitSeconds: 10,
throttleCommits: false,
useAsynchronousCommits: false,
sendFullCommit: true,
lmsCommitUrl: false,
dataCommitFormat: "json",
commitRequestDataType: "application/json;charset=UTF-8",
autoProgress: false,
logLevel: LogLevelEnum.ERROR,
selfReportSessionTime: false,
alwaysSendTotalTime: false,
renderCommonCommitFields: false,
autoCompleteLessonStatus: false,
strict_errors: true,
xhrHeaders: {},
xhrWithCredentials: false,
fetchMode: "cors",
asyncModeBeaconBehavior: "never",
responseHandler: async function(response) {
if (typeof response !== "undefined") {
let httpResult = null;
try {
if (typeof response.json === "function") {
httpResult = await response.json();
} else if (typeof response.text === "function") {
const responseText = await response.text();
if (responseText) {
httpResult = JSON.parse(responseText);
}
}
} catch (e) {
}
if (httpResult === null || !{}.hasOwnProperty.call(httpResult, "result")) {
if (response.status === 200) {
return {
result: global_constants.SCORM_TRUE,
errorCode: 0
};
} else {
return {
result: global_constants.SCORM_FALSE,
errorCode: 101
};
}
} else {
return {
result: httpResult.result,
errorCode: typeof httpResult.errorCode === "number" ? httpResult.errorCode : httpResult.result === true || httpResult.result === global_constants.SCORM_TRUE ? 0 : 101
};
}
}
return {
result: global_constants.SCORM_FALSE,
errorCode: 101
};
},
xhrResponseHandler: function(xhr) {
if (typeof xhr !== "undefined") {
let httpResult = null;
if (xhr.status >= 200 && xhr.status <= 299) {
try {
httpResult = JSON.parse(xhr.responseText);
} catch (e) {
}
if (httpResult === null || !{}.hasOwnProperty.call(httpResult, "result")) {
return { result: global_constants.SCORM_TRUE, errorCode: 0 };
}
return {
result: httpResult.result,
errorCode: typeof httpResult.errorCode === "number" ? httpResult.errorCode : httpResult.result === true || httpResult.result === global_constants.SCORM_TRUE ? 0 : 101
};
} else {
return { result: global_constants.SCORM_FALSE, errorCode: 101 };
}
}
return { result: global_constants.SCORM_FALSE, errorCode: 101 };
},
requestHandler: function(commitObject) {
return commitObject;
},
onLogMessage: defaultLogHandler,
mastery_override: false,
score_overrides_status: false,
completion_status_on_failed: "completed",
scoItemIds: [],
scoItemIdValidator: false,
globalObjectiveIds: [],
// Offline support settings
enableOfflineSupport: false,
courseId: "",
syncOnInitialize: true,
syncOnTerminate: true,
maxSyncAttempts: 5,
// Multi-SCO support settings
scoId: "",
autoPopulateCommitMetadata: false,
// HTTP service settings
httpService: null,
// Global learner preferences settings
globalStudentPreferences: false
};
function defaultLogHandler(messageLevel, logMessage) {
switch (messageLevel) {
case "4":
case 4:
case "ERROR":
case LogLevelEnum.ERROR:
console.error(logMessage);
break;
case "3":
case 3:
case "WARN":
case LogLevelEnum.WARN:
console.warn(logMessage);
break;
case "2":
case 2:
case "INFO":
case LogLevelEnum.INFO:
console.info(logMessage);
break;
case "1":
case 1:
case "DEBUG":
case LogLevelEnum.DEBUG:
if (console.debug) {
console.debug(logMessage);
} else {
console.log(logMessage);
}
break;
}
}
const scorm12_regex = {
/** CMIString256 - Character string, max 255 chars (RTE A.1) */
CMIString256: "^[\\s\\S]{0,255}$",
/** CMIString4096 - Character string, max 4096 chars (RTE A.1) */
CMIString4096: "^[\\s\\S]{0,4096}$",
/**
* CMIString64000 - Extended character string, max 64000 chars
*
* SPEC COMPLIANCE NOTE:
* The SCORM 1.2 specification defines cmi.suspend_data as CMIString4096 (max 4096 chars).
* This implementation intentionally increases the limit to 64000 chars (matching SCORM 2004)
* for the following reasons:
*
* 1. Modern content frequently exceeds 4096 chars due to JSON state serialization,
* base64 encoding, complex bookmark data, and rich interaction tracking
* 2. The 4096 limit was set in 2001 when content was simpler; modern authoring tools
* routinely generate larger suspend_data
* 3. Most LMS systems can handle larger values - the API shouldn't be the bottleneck
* 4. Content that gets rejected has no recovery path, causing data loss
* 5. Aligns with SCORM 2004's more practical 64000 char limit
*
* Used by: cmi.suspend_data (SCORM 1.2)
*
* Strict spec pattern would be: ^[\s\S]{0,4096}$
*/
CMIString64000: "^[\\s\\S]{0,64000}$",
/**
* CMITime - Clock time in HH:MM:SS.SS format (RTE A.2)
* Optional centiseconds (1-2 decimal digits) per spec.
*/
CMITime: "^(?:[01]\\d|2[0123]):(?:[012345]\\d):(?:[012345]\\d)(\\.\\d{1,2})?$",
/**
* CMITimespan - Time interval in HHHH:MM:SS.SS format (RTE A.3)
* We allow more digits for the hour to support values generated
* by getSecondsAsHHMMSS which can produce larger hour values
* (e.g., 17496:00:00 for very long durations).
* Changed from minimum 2 digits to 1+ digits with no upper limit.
*/
CMITimespan: "^([0-9]+):([0-9]{2}):([0-9]{2})(\\.\\d{1,2})?$",
/**
* CMIInteger - Non-negative integer (RTE A.4)
*
* SPEC COMPLIANCE NOTE:
* The SCORM 1.2 specification defines CMIInteger as 0-65536 range.
* This implementation intentionally omits range validation to support
* legacy content that may exceed this limit in _count fields or other
* integer values. Real-world content often violates the spec by storing
* larger values, and strict enforcement would break compatibility.
*
* Affected elements:
* - cmi.objectives._count
* - cmi.interactions._count
* - cmi.interactions.n.objectives._count
* - cmi.interactions.n.correct_responses._count
*/
CMIInteger: "^\\d+$",
/** CMISInteger - Signed integer (RTE A.5) */
CMISInteger: "^-?([0-9]+)$",
/**
* CMIDecimal - Signed decimal (RTE A.6)
* We set practical limits on decimals to prevent abuse while maintaining
* broad compatibility with legacy content.
* Increased from 3 to 10 digits before decimal to match SCORM 2004 behavior.
*/
CMIDecimal: "^-?([0-9]{0,10})(\\.[0-9]*)?$",
/**
* CMIIdentifier - Printable ASCII characters, max 255 chars (RTE A.7)
*
* SPEC COMPLIANCE NOTE:
* The SCORM 1.2 specification defines CMIIdentifier as alphanumeric only:
* letters (a-z, A-Z), numbers (0-9), hyphens (-), and underscores (_).
* Spaces and periods are explicitly NOT allowed per spec.
*
* This implementation intentionally relaxes validation to accept all
* printable ASCII characters (0x21-0x7E) plus whitespace to support
* legacy content. Many real-world LMS systems and content packages use
* identifiers that violate the strict spec (e.g., student IDs with spaces,
* objective IDs with periods or special characters).
*
* Strict spec pattern would be: ^[A-Za-z0-9_-]{0,255}$
*
* Affected elements:
* - cmi.core.student_id
* - cmi.objectives.n.id
* - cmi.interactions.n.id
* - cmi.interactions.n.objectives.n.id
*/
CMIIdentifier: "^[\\u0021-\\u007E\\s]{0,255}$",
/** CMICredit - Vocabulary: credit or no-credit (RTE 3.4.2.1.3) */
CMICredit: "^(credit|no-credit)$",
/** CMIEntry - Vocabulary: ab-initio, resume, or empty (RTE 3.4.2.1.4) */
CMIEntry: "^(ab-initio|resume|)$",
/** CMILessonMode - Vocabulary: normal, browse, or review (RTE 3.4.2.1.10) */
CMILessonMode: "^(normal|browse|review)$",
/** CMITimeLimitAction - Vocabulary: action combinations (RTE 3.4.2.1.11) */
CMITimeLimitAction: "^(exit,message|exit,no message|continue,message|continue,no message)$",
/**
* CMIFeedback - Relaxed for compatibility (normally CMIString255)
*
* SPEC COMPLIANCE NOTE:
* The SCORM 1.2 specification defines CMIFeedback as CMIString255 (max 255 chars)
* with format varying by interaction type (see RTE 3.4.2.7.5, 3.4.2.7.7).
*
* This implementation intentionally relaxes validation for two reasons:
*
* 1. LENGTH: Many legacy content packages store responses exceeding 255 chars,
* especially for fill-in and performance interaction types. Strict enforcement
* would break existing content with no user-facing benefit.
*
* 2. FORMAT: The spec requires type-specific formats (e.g., true-false accepts
* only "0"/"1"/"t"/"f", choice accepts comma-separated single chars). However:
* - Format validation requires knowing interaction type at validation time
* - Legacy content often uses non-standard formats
* - The SCO is responsible for response evaluation, not the API
* - Strict format validation provides minimal benefit vs. compatibility cost
*
* Affected elements:
* - cmi.interactions.n.student_response
* - cmi.interactions.n.correct_responses.n.pattern
*
* Strict spec pattern would be: ^[\s\S]{0,255}$ with type-specific subpatterns
*/
CMIFeedback: "^.*$",
/** CMIIndex - Pattern for array index extraction */
CMIIndex: "[._](\\d+).",
/** CMIStatus - Lesson status vocabulary (RTE 3.4.2.2.3) */
CMIStatus: "^(passed|completed|failed|incomplete|browsed)$",
/** CMIStatus2 - Extended status vocabulary with "not attempted" (RTE 3.4.2.6.2) */
CMIStatus2: "^(passed|completed|failed|incomplete|browsed|not attempted)$",
/** CMIExit - Exit vocabulary (RTE 3.4.2.1.5) */
CMIExit: "^(time-out|suspend|logout|)$",
/** CMIType - Interaction type vocabulary (RTE 3.4.2.7.2) */
CMIType: "^(true-false|choice|fill-in|matching|performance|sequencing|likert|numeric)$",
/** CMIResult - Interaction result vocabulary (RTE 3.4.2.7.6) */
CMIResult: "^(correct|wrong|unanticipated|neutral|([0-9]{0,3})?(\\.[0-9]*)?)$",
/** NAVEvent - Navigation event vocabulary (SCORM 1.2 extension) */
NAVEvent: "^(_?(previous|continue|start|resumeAll|exit|exitAll|abandon|abandonAll|suspendAll|retry|retryAll)|choice|jump|_none_)$",
/** score_range - Valid score range 0-100 (RTE 3.4.2.2.2) */
score_range: "0#100",
/** audio_range - Audio level range -1 to 100 (RTE 3.4.2.3.1) */
audio_range: "-1#100",
/** speed_range - Playback speed range -100 to 100 (RTE 3.4.2.3.2) */
speed_range: "-100#100",
/** weighting_range - Interaction weighting range -100 to 100 (RTE 3.4.2.7.4) */
weighting_range: "-100#100",
/** text_range - Text display preference -1 to 1 (RTE 3.4.2.3.3) */
text_range: "-1#1"
};
const scorm2004_regex = {
/** CMIString200 - Character string, max 200 chars (RTE C.1.1) */
CMIString200: "^[\\u0000-\\uFFFF]{0,200}$",
/** CMIString250 - Character string, max 250 chars (RTE C.1.1) */
CMIString250: "^[\\u0000-\\uFFFF]{0,250}$",
/** CMIString1000 - Character string, max 1000 chars (RTE C.1.1) */
CMIString1000: "^[\\u0000-\\uFFFF]{0,1000}$",
/** CMIString4000 - Character string, max 4000 chars (RTE C.1.1) */
CMIString4000: "^[\\u0000-\\uFFFF]{0,4000}$",
/** CMIString64000 - Character string, max 64000 chars (RTE C.1.1) */
CMIString64000: "^[\\u0000-\\uFFFF]{0,64000}$",
/**
* CMILang - Language code per RFC 1766/RFC 3066 (RTE C.1.2)
* Primary tag: 1-8 characters (ISO 639-1: 2, ISO 639-2: 3, or i/x for IANA/private)
* Subtag: 2-8 alphanumeric characters
*/
CMILang: "^([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?$|^$",
/** CMILangString250 - String with optional language tag, max 250 chars (RTE C.1.3) */
CMILangString250: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).{0,250}$)?$",
/** CMILangcr - Language tag pattern with content */
CMILangcr: "^(({lang=([a-zA-Z]{1,8}|i|x)?(-[a-zA-Z0-9-]{2,8})?}))(.*?)$",
/** CMILangString250cr - String with optional language tag (carriage return variant) */
CMILangString250cr: "^(({lang=([a-zA-Z]{1,8}|i|x)?(-[a-zA-Z0-9-]{2,8})?})?(.{0,250})?)?$",
/** CMILangString4000 - String with optional language tag, max 4000 chars (RTE C.1.3) */
CMILangString4000: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).{0,4000}$)?$",
/**
* CMITime - ISO 8601 timestamp format (RTE C.1.4)
* Year range expanded from 1970-2038 to 1970-9999 to support future dates
*/
CMITime: "^(19[7-9][0-9]|[2-9][0-9]{3})((-(0[1-9]|1[0-2]))((-(0[1-9]|[1-2][0-9]|3[0-1]))(T([0-1][0-9]|2[0-3])((:[0-5][0-9])((:[0-5][0-9])((\\.[0-9]{1,6})((Z|([+|-]([0-1][0-9]|2[0-3])))(:[0-5][0-9])?)?)?)?)?)?)?)?$",
/** CMITimespan - ISO 8601 duration format (RTE C.1.5) */
CMITimespan: "^P(?:([.,\\d]+)Y)?(?:([.,\\d]+)M)?(?:([.,\\d]+)W)?(?:([.,\\d]+)D)?(?:T?(?:([.,\\d]+)H)?(?:([.,\\d]+)M)?(?:(\\d+(?:\\.\\d{1,2})?)S)?)?$",
/** CMIInteger - Non-negative integer (RTE C.1.6) */
CMIInteger: "^\\d+$",
/** CMISInteger - Signed integer (RTE C.1.7) */
CMISInteger: "^-?([0-9]+)$",
/**
* CMIDecimal - Signed decimal (RTE C.1.8)
* Spec allows unlimited digits, but we set practical limits to prevent abuse
* while maintaining broad compatibility:
* - Up to 10 digits before decimal (supports values up to 10 billion)
* - Up to 18 digits after decimal (maintains precision for scientific use)
*/
CMIDecimal: "^-?([0-9]{1,10})(\\.[0-9]{1,18})?$",
/**
* CMIIdentifier - Identifier with alphanumeric ending, max 250 chars (RTE C.1.9)
* Must contain at least one word character (\w) and only allow: letters,
* numbers, - ( ) + . : = @ ; $ _ ! * ' % / #
* URN format is validated separately if string starts with "urn:"
*/
CMIIdentifier: "^(?=.*\\w)[\\w\\-\\(\\)\\+\\.\\:\\=\\@\\;\\$\\_\\!\\*\\'\\%\\/\\#]{1,250}$",
/** CMIShortIdentifier - Short identifier conforming to URI syntax, max 250 chars (RTE C.1.10) */
CMIShortIdentifier: "^(?=.*\\w)[\\w\\-\\(\\)\\+\\.\\:\\=\\@\\;\\$\\_\\!\\*\\'\\%\\/\\#]{1,250}$",
/** CMILongIdentifier - Long identifier supporting URN format, max 4000 chars (RTE C.1.11) */
CMILongIdentifier: "^(?:(?!urn:)\\S{1,4000}|urn:[A-Za-z0-9-]{1,31}:\\S{1,4000}|.{1,4000})$",
/** CMIFeedback - Unrestricted feedback text (RTE C.1.12) */
CMIFeedback: "^.*$",
/** CMIIndex - Pattern for array index extraction */
CMIIndex: "[._](\\d+).",
/** CMIIndexStore - Pattern for stored index notation */
CMIIndexStore: ".N(\\d+).",
/** CMICStatus - Completion status vocabulary (RTE 4.1.4) */
CMICStatus: "^(completed|incomplete|not attempted|unknown)$",
/** CMISStatus - Success status vocabulary (RTE 4.1.11) */
CMISStatus: "^(passed|failed|unknown)$",
/** CMIExit - Exit vocabulary (RTE 4.1.3) */
CMIExit: "^(time-out|suspend|logout|normal)$",
/** CMIType - Interaction type vocabulary (RTE 4.1.6.2) */
CMIType: "^(true-false|choice|fill-in|long-fill-in|matching|performance|sequencing|likert|numeric|other)$",
/** CMIResult - Interaction result vocabulary (RTE 4.1.6.8) */
CMIResult: "^(correct|incorrect|unanticipated|neutral|-?([0-9]{1,4})(\\.[0-9]{1,18})?)$",
/** NAVEvent - Navigation event vocabulary (SN Book Table 4.4.2) */
NAVEvent: "^(_?(start|resumeAll|previous|continue|exit|exitAll|abandon|abandonAll|suspendAll|retry|retryAll)|_none_|(\\{target=(?<choice_target>\\S{0,}[a-zA-Z0-9-_]+)})?choice|(\\{target=(?<jump_target>\\S{0,}[a-zA-Z0-9-_]+)})?jump)$",
/** NAVBoolean - Navigation boolean vocabulary (SN Book) */
NAVBoolean: "^(unknown|true|false)$",
/** NAVTarget - Navigation target pattern (SN Book) */
NAVTarget: "^{target=\\S{0,}[a-zA-Z0-9-_]+}$",
/** scaled_range - Scaled score range -1 to 1 (RTE 4.1.10.1) */
scaled_range: "-1#1",
/** audio_range - Audio level range 0 to 999.9999999 (RTE 4.1.7.1) */
audio_range: "0#999.9999999",
/** speed_range - Playback speed range 0 to 999.9999999 (RTE 4.1.7.4) */
speed_range: "0#999.9999999",
/** text_range - Text display preference -1 to 1 (RTE 4.1.7.5) */
text_range: "-1#1",
/** progress_range - Progress measure range 0 to 1 (RTE 4.1.8) */
progress_range: "0#1"
};
class ScheduledCommit {
_API;
_cancelled = false;
_timeout;
_callback;
/**
* Constructor for ScheduledCommit
* @param {BaseAPI} API
* @param {number} when
* @param {string} callback
*/
constructor(API, when, callback) {
this._API = API;
this._timeout = setTimeout(this.wrapper.bind(this), when);
this._callback = callback;
}
/**
* Cancel any currently scheduled commit
*/
cancel() {
this._cancelled = true;
if (this._timeout) {
clearTimeout(this._timeout);
}
}
/**
* Wrap the API commit call to check if the call has already been canceled
*/
wrapper() {
if (!this._cancelled) {
if (this._API.isInitialized()) {
(async () => await this._API.commit(this._callback))();
}
}
}
}
class AsynchronousHttpService {
settings;
error_codes;
/**
* Constructor for AsynchronousHttpService
* @param {Settings} settings - The settings object
* @param {ErrorCode} error_codes - The error codes object
*/
constructor(settings, error_codes) {
this.settings = settings;
this.error_codes = error_codes;
}
/**
* Sends HTTP requests asynchronously to the LMS
* Returns immediate success - actual result handled via events
*
* WARNING: This is NOT SCORM-compliant. Always returns optimistic success immediately.
* The actual HTTP request happens in the background, and success/failure is reported
* via CommitSuccess/CommitError events, but NOT to the SCO's commit call.
*
* @param {string} url - The URL endpoint to send the request to
* @param {CommitObject|StringKeyMap|Array} params - The data to send to the LMS
* @param {boolean} immediate - Whether to send the request immediately without waiting
* @param {Function} apiLog - Function to log API messages with appropriate levels
* @param {Function} processListeners - Function to trigger event listeners for commit events
* @return {ResultObject} - Immediate optimistic success result
*/
processHttpRequest(url, params, immediate = false, apiLog, processListeners) {
this._performAsyncRequest(url, params, immediate, apiLog, processListeners);
return {
result: global_constants.SCORM_TRUE,
errorCode: 0
};
}
/**
* Performs the async request in the background
* @param {string} url - The URL to send the request to
* @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
* @param {boolean} immediate - Whether this is an immediate request
* @param apiLog - Function to log API messages
* @param {Function} processListeners - Function to process event listeners
* @private
*/
async _performAsyncRequest(url, params, immediate, apiLog, processListeners) {
try {
const processedParams = this.settings.requestHandler(params);
let response;
if (immediate && this.settings.asyncModeBeaconBehavior !== "never") {
response = await this.performBeacon(url, processedParams);
} else {
response = await this.performFetch(url, processedParams);
}
const result = await this.transformResponse(response, processListeners);
if (this._isSuccessResponse(response, result)) {
processListeners("CommitSuccess");
} else {
processListeners("CommitError", void 0, result.errorCode);
}
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
apiLog("processHttpRequest", `Async request failed: ${message}`, LogLevelEnum.ERROR);
processListeners("CommitError");
}
}
/**
* Prepares the request body and content type based on params type
* @param {CommitObject|StringKeyMap|Array} params - The parameters to include in the request
* @return {Object} - Object containing body and contentType
* @private
*/
_prepareRequestBody(params) {
const body = params instanceof Array ? params.join("&") : JSON.stringify(params);
const contentType = params instanceof Array ? "application/x-www-form-urlencoded" : this.settings.commitRequestDataType;
return { body, contentType };
}
/**
* Perform the fetch request to the LMS
* @param {string} url - The URL to send the request to
* @param {StringKeyMap|Array} params - The parameters to include in the request
* @return {Promise<Response>} - The response from the LMS
* @private
*/
async performFetch(url, params) {
if (this.settings.asyncModeBeaconBehavior === "always") {
return this.performBeacon(url, params);
}
const { body, contentType } = this._prepareRequestBody(params);
const init = {
method: "POST",
mode: this.settings.fetchMode,
body,
headers: {
...this.settings.xhrHeaders,
"Content-Type": contentType
},
keepalive: true
};
if (this.settings.xhrWithCredentials) {
init.credentials = "include";
}
return fetch(url, init);
}
/**
* Perform the beacon request to the LMS
* @param {string} url - The URL to send the request to
* @param {StringKeyMap|Array} params - The parameters to include in the request
* @return {Promise<Response>} - A promise that resolves with a mock Response object
* @private
*/
async performBeacon(url, params) {
const { body, contentType } = this._prepareRequestBody(params);
const beaconSuccess = navigator.sendBeacon(url, new Blob([body], { type: contentType }));
return Promise.resolve({
status: beaconSuccess ? 200 : 0,
ok: beaconSuccess,
json: async () => ({
result: beaconSuccess ? "true" : "false",
errorCode: beaconSuccess ? 0 : this.error_codes.GENERAL_COMMIT_FAILURE || 391
}),
text: async () => JSON.stringify({
result: beaconSuccess ? "true" : "false",
errorCode: beaconSuccess ? 0 : this.error_codes.GENERAL_COMMIT_FAILURE || 391
})
});
}
/**
* Transforms the response from the LMS to a ResultObject
* @param {Response} response - The response from the LMS
* @param {Function} processListeners - Function to process event listeners
* @return {Promise<ResultObject>} - The transformed response
* @private
*/
async transformResponse(response, processListeners) {
let result;
try {
result = typeof this.settings.responseHandler === "function" ? await this.settings.responseHandler(response) : await response.json();
} catch (parseError) {
const responseText = await response.text().catch(() => "Unable to read response text");
return {
result: global_constants.SCORM_FALSE,
errorCode: this.error_codes.GENERAL_COMMIT_FAILURE || 391,
errorMessage: `Failed to parse LMS response: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
errorDetails: JSON.stringify({
status: response.status,
statusText: response.statusText,
url: response.url,
responseText: responseText.substring(0, 500),
// Limit response text to avoid huge logs
parseError: parseError instanceof Error ? parseError.message : String(parseError)
})
};
}
if (!Object.hasOwnProperty.call(result, "errorCode")) {
result.errorCode = this._isSuccessResponse(response, result) ? 0 : this.error_codes.GENERAL_COMMIT_FAILURE || 391;
}
if (!this._isSuccessResponse(response, result)) {
result.errorDetails = {
status: response.status,
statusText: response.statusText,
url: response.url,
...result.errorDetails
// Preserve any existing error details
};
}
return result;
}
/**
* Determines if a response is successful based on status code and result
* @param {Response} response - The HTTP response
* @param {ResultObject} result - The parsed result object
* @return {boolean} - Whether the response is successful
* @private
*/
_isSuccessResponse(response, result) {
const value = result.result;
return response.status >= 200 && response.status <= 299 && (value === true || value === "true" || value === global_constants.SCORM_TRUE);
}
/**
* Updates the service settings
* @param {Settings} settings - The new settings
*/
updateSettings(settings) {
this.settings = settings;
}
}
const TARGET_ATTRIBUTE_PREFIX = "{target=";
function getErrorCode(errorCodes, key) {
const code = errorCodes[key];
if (code === void 0) {
if (typeof console !== "undefined" && console.warn) {
console.warn(`CMIValueAccessService: Unknown error code key: ${key}`);
}
return errorCodes["GENERAL"] ?? 0;
}
return code;
}
class CMIValueAccessService {
context;
constructor(context) {
this.context = context;
}
/**
* Gets the appropriate error code for undefined data model elements.
* SCORM 2004 uses UNDEFINED_DATA_MODEL, SCORM 1.2 uses GENERAL.
*/
getUndefinedDataModelErrorCode(scorm2004) {
return scorm2004 ? getErrorCode(this.context.errorCodes, "UNDEFINED_DATA_MODEL") : getErrorCode(this.context.errorCodes, "GENERAL");
}
/**
* Sets a value on a CMI element path
*
* @param {string} methodName - The API method name for logging
* @param {boolean} scorm2004 - Whether this is SCORM 2004
* @param {string} CMIElement - The CMI element path
* @param {string} value - The value to set (all SCORM values are strings)
* @return {string} "true" or "false"
*/
setCMIValue(methodName, scorm2004, CMIElement, value) {
if (!CMIElement || CMIElement === "") {
if (scorm2004) {
this.context.throwSCORMError(
CMIElement,
getErrorCode(this.context.errorCodes, "GENERAL_SET_FAILURE"),
"The data model element was not specified"
);
}
return global_constants.SCORM_FALSE;
}
this.context.setLastErrorCode("0");
const structure = CMIElement.split(".");
let refObject = this.context.getDataModel();
let returnValue = global_constants.SCORM_FALSE;
let foundFirstIndex = false;
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
const invalidErrorCode = this.getUndefinedDataModelErrorCode(scorm2004);
for (let idx = 0; idx < structure.length; idx++) {
const attribute = structure[idx];
if (idx === structure.length - 1) {
returnValue = this.setFinalAttribute(
refObject,
attribute,
value,
CMIElement,
scorm2004,
invalidErrorCode,
invalidErrorMessage
);
break;
} else {
const traverseResult = this.traverseToNextLevel(
refObject,
structure,
idx,
value,
CMIElement,
scorm2004,
foundFirstIndex,
invalidErrorCode,
invalidErrorMessage
);
if (traverseResult.error) {
break;
}
refObject = traverseResult.refObject;
idx = traverseResult.idx;
foundFirstIndex = traverseResult.foundFirstIndex;
}
}
if (returnValue === global_constants.SCORM_FALSE) {
this.context.apiLog(
methodName,
`There was an error setting the value for: ${CMIElement}, value of: ${value}`,
LogLevelEnum.WARN
);
}
return returnValue;
}
/**
* Gets a value from a CMI element path
*
* @param {string} methodName - The API method name for logging
* @param {boolean} scorm2004 - Whether this is SCORM 2004
* @param {string} CMIElement - The CMI element path
* @return The value at the element path, or empty string on error (per SCORM spec)
*/
getCMIValue(methodName, scorm2004, CMIElement) {
if (!CMIElement || CMIElement === "") {
if (scorm2004) {
this.context.throwSCORMError(
CMIElement,
getErrorCode(this.context.errorCodes, "GENERAL_GET_FAILURE"),
"The data model element was not specified"
);
}
return "";
}
if (scorm2004 && CMIElement.endsWith("._version") && CMIElement !== "cmi._version") {
this.context.throwSCORMError(
CMIElement,
getErrorCode(this.context.errorCodes, "GENERAL_GET_FAILURE"),
"The _version keyword was used incorrectly"
);
return "";
}
const structure = CMIElement.split(".");
let refObject = this.context.getDataModel();
let attribute = null;
const uninitializedErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) has not been initialized.`;
const invalidErrorMessage = `The data model element passed to ${methodName} (${CMIElement}) is not a valid SCORM data model element.`;
const invalidErrorCode = this.getUndefinedDataModelErrorCode(scorm2004);
for (let idx = 0; idx < structure.length; idx++) {
attribute = structure[idx];
const validationResult = this.validateGetAttribute(
refObject,
attribute,
CMIElement,
scorm2004,
invalidErrorCode,
invalidErrorMessage,
idx === structure.length - 1
);
if (validationResult.returnValue !== void 0) {
return validationResult.returnValue;
}
if (validationResult.error) {
return "";
}
if (attribute !== void 0 && attribute !== null) {
refObject = refObject[attribute];
if (refObject === void 0) {
this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
break;
}
} else {
this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
break;
}
if (refObject instanceof CMIArray) {
const arrayResult = this.handleGetArrayAccess(
refObject,
structure,
idx,
CMIElement,
uninitializedErrorMessage
);
if (arrayResult.error) {
return "";
}
refObject = arrayResult.refObject;
idx = arrayResult.idx;
}
}
if (refObject === null || refObject === void 0) {
if (!scorm2004) {
if (attribute === "_children") {
this.context.throwSCORMError(
CMIElement,
getErrorCode(this.context.errorCodes, "CHILDREN_ERROR"),
void 0
);
} else if (attribute === "_count") {
this.context.throwSCORMError(
CMIElement,
getErrorCode(this.context.errorCodes, "COUNT_ERROR"),
void 0
);
}
}
return "";
}
return refObject;
}
/**
* Sets the final attribute value in the CMI path
*/
setFinalAttribute(refObject, attribute, value, CMIElement, scorm2004, invalidErrorCode, invalidErrorMessage) {
if (scorm2004 && attribute?.startsWith(TARGET_ATTRIBUTE_PREFIX)) {
if (this.context.isInitialized()) {
this.context.throwSCORMError(
CMIElement,
getErrorCode(this.context.errorCodes, "READ_ONLY_ELEMENT")
);
return global_constants.SCORM_FALSE;
}
return global_constants.SCORM_TRUE;
}
if (typeof attribute === "undefined" || !this.context.checkObjectHasProperty(refObject, attribute)) {
this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
return global_constants.SCORM_FALSE;
}
if (stringMatches(CMIElement, "\\.correct_responses\\.\\d+$") && this.context.isInitialized() && attribute !== "pattern") {
this.context.validateCorrectResponse(CMIElement, value);
if (this.context.getLastErrorCode() !== "0") {
this.context.throwSCORMError(
CMIElement,
getErrorCode(this.context.errorCodes, "TYPE_MISMATCH")
);
return global_constants.SCORM_FALSE;
}
}
if (!scorm2004 || this.context.getLastErrorCode() === "0") {
if (typeof attribute === "undefined" || attribute === "__proto__" || attribute === "constructor") {
this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
return global_constants.SCORM_FALSE;
}
if (scorm2004 && attribute === "id" && this.context.isInitialized()) {
const duplicateError = this.context.checkForDuplicateId(CMIElement, value);
if (duplicateError) {
this.context.throwSCORMError(
CMIElement,
getErrorCode(this.context.errorCodes, "GENERAL_SET_FAILURE")
);
return global_constants.SCORM_FALSE;
}
}
refObject[attribute] = value;
return global_constants.SCORM_TRUE;
}
return global_constants.SCORM_FALSE;
}
/**
* Traverses to the next level in the CMI path
*/
traverseToNextLevel(refObject, structure, idx, value, CMIElement, scorm2004, foundFirstIndex, invalidErrorCode, invalidErrorMessage) {
const attribute = structure[idx];
if (typeof attribute === "undefined" || !this.context.checkObjectHasProperty(refObject, attribute)) {
this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
return { refObject, idx, foundFirstIndex, error: true };
}
refObject = refObject[attribute];
if (!refObject) {
this.context.throwSCORMError(CMIElement, invalidErrorCode, invalidErrorMessage);
return { refObject, idx, foundFirstIndex, error: true };
}
if (refObject instanceof CMIArray) {
const arrayResult = this.handleSetArrayAccess(
refObject,
structure,
idx,
value,
CMIElement,
scorm2004,
foundFirstIndex,
invalidErrorCode,
invalidErrorMessage
);
if (arrayResult.error) {
return { refObject, idx, foundFirstIndex, error: true };
}
return arrayResult;
}
return { refObject, idx, foundFirstIndex, error: false };
}
/**
* Handles array access during set operations
*/
handleSetArrayAccess(refObject, structure, idx, value, CMIElement, scorm2004, foundFirstIndex, invalidErrorCode, invalidErrorMessage) {
const index = parseInt(structure[idx + 1] || "0", 10);
if (!isNaN(index)) {
const item = refObject.childArray[index];
if (item) {
return {
refObject: item,
idx: idx + 1,
foundFirstIndex: true,