scorm-again
Version:
A modern SCORM JavaScript run-time library for SCORM 1.2 and SCORM 2004
1,822 lines (1,807 loc) • 770 kB
JavaScript
const SECONDS_PER_SECOND = 1;
const SECONDS_PER_MINUTE = 60;
const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
const designations = {
D: SECONDS_PER_DAY,
H: SECONDS_PER_HOUR,
M: SECONDS_PER_MINUTE,
S: SECONDS_PER_SECOND
};
const getSecondsAsISODuration = (seconds) => {
if (!seconds || seconds <= 0) {
return "PT0S";
}
let duration = "P";
let remainder = seconds;
const designationEntries = Object.entries(designations);
designationEntries.forEach(([designationsKey, current_seconds]) => {
let value = Math.floor(remainder / current_seconds);
remainder = remainder % current_seconds;
if (countDecimals(remainder) > 2) {
remainder = Number(Number(remainder).toFixed(2));
}
if (designationsKey === "S" && remainder > 0) {
value += remainder;
}
if (value) {
const needsTimeSeparator = (duration.indexOf("D") > 0 || ["H", "M", "S"].includes(designationsKey)) && duration.indexOf("T") === -1;
if (needsTimeSeparator) {
duration += "T";
}
duration += `${value}${designationsKey}`;
}
});
return duration;
};
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}`;
}
);
const validateISO8601Duration = memoize(
(duration, durationRegex) => {
if (typeof durationRegex === "string") {
durationRegex = new RegExp(durationRegex);
}
return !(!duration || !duration?.match?.(durationRegex));
}
);
function addTwoDurations(first, second, durationRegex) {
const regex = new RegExp(durationRegex) ;
return getSecondsAsISODuration(
getDurationAsSeconds(first, regex) + getDurationAsSeconds(second, regex)
);
}
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;
})();
});
}
function parseNavigationRequest(navRequest) {
const validCommands = /* @__PURE__ */ new Set([
"start",
"resumeAll",
"continue",
"previous",
"choice",
"jump",
"exit",
"exitAll",
"abandon",
"abandonAll",
"suspendAll",
"_none_"
]);
const trimmed = navRequest.trim();
if (!trimmed) {
return {
command: "_none_",
targetActivityId: null,
valid: false,
error: "Empty navigation request"
};
}
if (validCommands.has(trimmed)) {
return {
command: trimmed,
targetActivityId: null,
valid: true
};
}
const dotIndex = trimmed.indexOf(".");
if (dotIndex > 0) {
const command = trimmed.substring(0, dotIndex);
const targetActivityId = trimmed.substring(dotIndex + 1);
if ((command === "choice" || command === "jump") && targetActivityId) {
if (/^[a-zA-Z0-9._-]+$/.test(targetActivityId)) {
return {
command,
targetActivityId,
valid: true
};
} else {
return {
command: "_none_",
targetActivityId: null,
valid: false,
error: `Invalid target activity ID: contains disallowed characters`
};
}
}
}
return {
command: "_none_",
targetActivityId: null,
valid: false,
error: `Unrecognized navigation command: "${trimmed}"`
};
}
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 = {
score_children: "raw,min,max",
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 scorm2004_constants = {
// Children lists
cmi_children: "_version,comments_from_learner,comments_from_lms,completion_status,completion_threshold,credit,entry,exit,interactions,launch_data,learner_id,learner_name,learner_preference,location,max_time_allowed,mode,objectives,progress_measure,scaled_passing_score,score,session_time,success_status,suspend_data,time_limit_action,total_time",
comments_children: "comment,timestamp,location",
score_children: "max,raw,scaled,min",
objectives_children: "progress_measure,completion_status,success_status,description,score,id",
correct_responses_children: "pattern",
student_preference_children: "audio_level,audio_captioning,delivery_speed,language",
interactions_children: "id,type,objectives,timestamp,correct_responses,weighting,learner_response,result,latency,description",
adl_data_children: "id,store",
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."
},
"102": {
basicMessage: "General Initialization Failure",
detailMessage: "Call to Initialize failed for an unknown reason."
},
"103": {
basicMessage: "Already Initialized",
detailMessage: "Call to Initialize failed because Initialize was already called."
},
"104": {
basicMessage: "Content Instance Terminated",
detailMessage: "Call to Initialize failed because Terminate was already called."
},
"111": {
basicMessage: "General Termination Failure",
detailMessage: "Call to Terminate failed for an unknown reason."
},
"112": {
basicMessage: "Termination Before Initialization",
detailMessage: "Call to Terminate failed because it was made before the call to Initialize."
},
"113": {
basicMessage: "Termination After Termination",
detailMessage: "Call to Terminate failed because Terminate was already called."
},
"122": {
basicMessage: "Retrieve Data Before Initialization",
detailMessage: "Call to GetValue failed because it was made before the call to Initialize."
},
"123": {
basicMessage: "Retrieve Data After Termination",
detailMessage: "Call to GetValue failed because it was made after the call to Terminate."
},
"132": {
basicMessage: "Store Data Before Initialization",
detailMessage: "Call to SetValue failed because it was made before the call to Initialize."
},
"133": {
basicMessage: "Store Data After Termination",
detailMessage: "Call to SetValue failed because it was made after the call to Terminate."
},
"142": {
basicMessage: "Commit Before Initialization",
detailMessage: "Call to Commit failed because it was made before the call to Initialize."
},
"143": {
basicMessage: "Commit After Termination",
detailMessage: "Call to Commit failed because it was made after the call to Terminate."
},
"201": {
basicMessage: "General Argument Error",
detailMessage: "An invalid argument was passed to an API method (usually indicates that Initialize, Commit or Terminate did not receive the expected empty string argument."
},
"301": {
basicMessage: "General Get Failure",
detailMessage: "Indicates a failed GetValue call where no other specific error code is applicable. Use GetDiagnostic for more information."
},
"351": {
basicMessage: "General Set Failure",
detailMessage: "Indicates a failed SetValue call where no other specific error code is applicable. Use GetDiagnostic for more information."
},
"391": {
basicMessage: "General Commit Failure",
detailMessage: "Indicates a failed Commit call where no other specific error code is applicable. Use GetDiagnostic for more information."
},
"401": {
basicMessage: "Undefined Data Model Element",
detailMessage: "The data model element name passed to GetValue or SetValue is not a valid SCORM data model element."
},
"402": {
basicMessage: "Unimplemented Data Model Element",
detailMessage: "The data model element indicated in a call to GetValue or SetValue is valid, but was not implemented by this LMS. In SCORM 2004, this error would indicate an LMS that is not fully SCORM conformant."
},
"403": {
basicMessage: "Data Model Element Value Not Initialized",
detailMessage: "Attempt to read a data model element that has not been initialized by the LMS or through a SetValue call. This error condition is often reached during normal execution of a SCO."
},
"404": {
basicMessage: "Data Model Element Is Read Only",
detailMessage: "SetValue was called with a data model element that can only be read."
},
"405": {
basicMessage: "Data Model Element Is Write Only",
detailMessage: "GetValue was called on a data model element that can only be written to."
},
"406": {
basicMessage: "Data Model Element Type Mismatch",
detailMessage: "SetValue was called with a value that is not consistent with the data format of the supplied data model element."
},
"407": {
basicMessage: "Data Model Element Value Out Of Range",
detailMessage: "The numeric value supplied to a SetValue 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 scorm2004_errors$1 = scorm2004_constants.error_descriptions;
class Scorm2004ValidationError extends ValidationError {
/**
* Constructor to take in an error code
* @param {string} CMIElement
* @param {number} errorCode
*/
constructor(CMIElement, errorCode) {
if ({}.hasOwnProperty.call(scorm2004_errors$1, String(errorCode))) {
super(
CMIElement,
errorCode,
scorm2004_errors$1[String(errorCode)]?.basicMessage || "Unknown error",
scorm2004_errors$1[String(errorCode)]?.detailMessage
);
} else {
super(
CMIElement,
101,
scorm2004_errors$1["101"]?.basicMessage,
scorm2004_errors$1["101"]?.detailMessage
);
}
Object.setPrototypeOf(this, Scorm2004ValidationError.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,
INVALID_SET_VALUE: 402,
READ_ONLY_ELEMENT: 403,
TYPE_MISMATCH: 405,
VALUE_OUT_OF_RANGE: 407};
const scorm2004_errors = {
...global_errors,
INITIALIZATION_FAILED: 102,
INITIALIZED: 103,
TERMINATED: 104,
TERMINATION_FAILURE: 111,
TERMINATION_BEFORE_INIT: 112,
MULTIPLE_TERMINATION: 113,
MULTIPLE_TERMINATIONS: 113,
RETRIEVE_BEFORE_INIT: 122,
RETRIEVE_AFTER_TERM: 123,
STORE_BEFORE_INIT: 132,
STORE_AFTER_TERM: 133,
COMMIT_BEFORE_INIT: 142,
COMMIT_AFTER_TERM: 143,
ARGUMENT_ERROR: 201,
GENERAL_GET_FAILURE: 301,
GENERAL_SET_FAILURE: 351,
GENERAL_COMMIT_FAILURE: 391,
UNDEFINED_DATA_MODEL: 401,
UNIMPLEMENTED_ELEMENT: 402,
VALUE_NOT_INITIALIZED: 403,
READ_ONLY_ELEMENT: 404,
WRITE_ONLY_ELEMENT: 405,
TYPE_MISMATCH: 406,
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 NAVBoolean = {
UNKNOWN: "unknown",
TRUE: "true",
FALSE: "false"
};
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}$",
/** 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]*)?$",
/** 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",
/** text_range - Text display preference -1 to 1 (RTE 3.4.2.3.3) */
text_range: "-1#1"
};
const scorm2004_regex = {
/** 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)?)?$",
/** 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})?$",
/** 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: "^.*$",
/** 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"
};
const PERFORMANCE_STEP_NAME = "^$|" + scorm2004_regex.CMIShortIdentifier;
const PERFORMANCE_CHARACTERSTRING = "(?![\\s\\S]*(?:\\[,\\]|\\[\\.\\]|\\[:\\]))[\\s\\S]{1,250}";
const PERFORMANCE_NUMERIC_RANGE = "(?:-?\\d+(?:\\.\\d+)?)?\\[:\\](?:-?\\d+(?:\\.\\d+)?)?";
const CR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_NUMERIC_RANGE + "|" + PERFORMANCE_CHARACTERSTRING + ")$";
const LR_PERFORMANCE_STEP_ANSWER = "^(?:|" + PERFORMANCE_CHARACTERSTRING + ")$";
const LearnerResponses = {
"true-false": {
format: "^true$|^false$",
max: 1,
delimiter: "",
unique: false
},
choice: {
format: scorm2004_regex.CMILongIdentifier,
max: 36,
delimiter: "[,]",
unique: true
},
"fill-in": {
format: scorm2004_regex.CMILangString250,
max: 10,
delimiter: "[,]",
unique: false
},
"long-fill-in": {
format: scorm2004_regex.CMILangString4000,
max: 1,
delimiter: "",
unique: false
},
matching: {
format: scorm2004_regex.CMIShortIdentifier,
format2: scorm2004_regex.CMIShortIdentifier,
max: 36,
delimiter: "[,]",
delimiter2: "[.]",
unique: false
},
performance: {
format: PERFORMANCE_STEP_NAME,
format2: LR_PERFORMANCE_STEP_ANSWER,
max: 250,
delimiter: "[,]",
delimiter2: "[.]",
unique: false
},
sequencing: {
format: scorm2004_regex.CMIShortIdentifier,
max: 36,
delimiter: "[,]",
unique: false
},
likert: {
format: scorm2004_regex.CMIShortIdentifier,
max: 1,
delimiter: "",
unique: false
},
numeric: {
format: scorm2004_regex.CMIDecimal,
max: 1,
delimiter: "",
unique: false
},
other: {
format: scorm2004_regex.CMIString4000,
max: 1,
delimiter: "",
unique: false
}
};
const CorrectResponses = {
"true-false": {
max: 1,
delimiter: "",
unique: false,
duplicate: false,
format: "^true$|^false$",
limit: 1
},
choice: {
max: 36,
delimiter: "[,]",
unique: true,
duplicate: false,
format: scorm2004_regex.CMILongIdentifier
},
"fill-in": {
max: 10,
delimiter: "[,]",
unique: false,
duplicate: false,
format: scorm2004_regex.CMILangString250cr
},
"long-fill-in": {
max: 1,
delimiter: "",
unique: false,
duplicate: true,
format: scorm2004_regex.CMILangString4000
},
matching: {
max: 36,
delimiter: "[,]",
delimiter2: "[.]",
unique: false,
duplicate: false,
format: scorm2004_regex.CMIShortIdentifier,
format2: scorm2004_regex.CMIShortIdentifier
},
performance: {
max: 250,
delimiter: "[,]",
delimiter2: "[.]",
unique: false,
duplicate: false,
// step_name: optional short_identifier_type
format: PERFORMANCE_STEP_NAME,
// step_answer: optional characterstring (spaces allowed) or numeric range
format2: CR_PERFORMANCE_STEP_ANSWER
},
sequencing: {
max: 36,
delimiter: "[,]",
unique: false,
duplicate: false,
format: scorm2004_regex.CMIShortIdentifier
},
likert: {
max: 1,
delimiter: "",
unique: false,
duplicate: false,
format: scorm2004_regex.CMIShortIdentifier,
limit: 1
},
numeric: {
max: 2,
delimiter: "[:]",
unique: false,
duplicate: false,
format: scorm2004_regex.CMIDecimal,
limit: 1
},
other: {
max: 1,
delimiter: "",
unique: false,
duplicate: false,
format: scorm2004_regex.CMIString4000,
limit: 1
}
};
const ValidLanguages = [
"aa",
"ab",
"ae",
"af",
"ak",
"am",
"an",
"ar",
"as",
"av",
"ay",
"az",
"ba",
"be",
"bg",
"bh",
"bi",
"bm",
"bn",
"bo",
"br",
"bs",
"ca",
"ce",
"ch",
"co",
"cr",
"cs",
"cu",
"cv",
"cy",
"da",
"de",
"dv",
"dz",
"ee",
"el",
"en",
"eo",
"es",
"et",
"eu",
"fa",
"ff",
"fi",
"fj",
"fo",
"fr",
"fy",
"ga",
"gd",
"gl",
"gn",
"gu",
"gv",
"ha",
"he",
"hi",
"ho",
"hr",
"ht",
"hu",
"hy",
"hz",
"ia",
"id",
"ie",
"ig",
"ii",
"ik",
"io",
"is",
"it",
"iu",
"ja",
"jv",
"ka",
"kg",
"ki",
"kj",
"kk",
"kl",
"km",
"kn",
"ko",
"kr",
"ks",
"ku",
"kv",
"kw",
"ky",
"la",
"lb",
"lg",
"li",
"ln",
"lo",
"lt",
"lu",
"lv",
"mg",
"mh",
"mi",
"mk",
"ml",
"mn",
"mo",
"mr",
"ms",
"mt",
"my",
"na",
"nb",
"nd",
"ne",
"ng",
"nl",
"nn",
"no",
"nr",
"nv",
"ny",
"oc",
"oj",
"om",
"or",
"os",
"pa",
"pi",
"pl",
"ps",
"pt",
"qu",
"rm",
"rn",
"ro",
"ru",
"rw",
"sa",
"sc",
"sd",
"se",
"sg",
"sh",
"si",
"sk",
"sl",
"sm",
"sn",
"so",
"sq",
"sr",
"ss",
"st",
"su",
"sv",
"sw",
"ta",
"te",
"tg",
"th",
"ti",
"tk",
"tl",
"tn",
"to",
"tr",
"ts",
"tt",
"tw",
"ty",
"ug",
"uk",
"ur",
"uz",
"ve",
"vi",
"vo",
"wa",
"wo",
"xh",
"yi",
"yo",
"za",
"zh",
"zu",
"aar",
"abk",
"ave",
"afr",
"aka",
"amh",
"arg",
"ara",
"asm",
"ava",
"aym",
"aze",
"bak",
"bel",
"bul",
"bih",
"bis",
"bam",
"ben",
"tib",
"bod",
"bre",
"bos",
"cat",
"che",
"cha",
"cos",
"cre",
"cze",
"ces",
"chu",
"chv",
"wel",
"cym",
"dan",
"ger",
"deu",
"div",
"dzo",
"ewe",
"gre",
"ell",
"eng",
"epo",
"spa",
"est",
"baq",
"eus",
"per",
"fas",
"ful",
"fin",
"fij",
"fao",
"fre",
"fra",
"fry",
"gle",
"gla",
"glg",
"grn",
"guj",
"glv",
"hau",
"heb",
"hin",
"hmo",
"hrv",
"hat",
"hun",
"arm",
"hye",
"her",
"ina",
"ind",
"ile",
"ibo",
"iii",
"ipk",
"ido",
"ice",
"isl",
"ita",
"iku",
"jpn",
"jav",
"geo",
"kat",
"kon",
"kik",
"kua",
"kaz",
"kal",
"khm",
"kan",
"kor",
"kau",
"kas",
"kur",
"kom",
"cor",
"kir",
"lat",
"ltz",
"lug",
"lim",
"lin",
"lao",
"lit",
"lub",
"lav",
"mlg",
"mah",
"mao",
"mri",
"mac",
"mkd",
"mal",
"mon",
"mol",
"mar",
"may",
"msa",
"mlt",
"bur",
"mya",
"nau",
"nob",
"nde",
"nep",
"ndo",
"dut",
"nld",
"nno",
"nor",
"nbl",
"nav",
"nya",
"oci",
"oji",
"orm",
"ori",
"oss",
"pan",
"pli",
"pol",
"pus",
"por",
"que",
"roh",
"run",
"rum",
"ron",
"rus",
"kin",
"san",
"srd",
"snd",
"sme",
"sag",
"slo",
"sin",
"slk",
"slv",
"smo",
"sna",
"som",
"alb",
"sqi",
"srp",
"ssw",
"sot",
"sun",
"swe",
"swa",
"tam",
"tel",
"tgk",
"tha",
"tir",
"tuk",
"tgl",
"tsn",
"ton",
"tur",
"tso",
"tat",
"twi",
"tah",
"uig",
"ukr",
"urd",
"uzb",
"ven",
"vie",
"vol",
"wln",
"wol",
"xho",
"yid",
"yor",
"zha",
"chi",
"zho",
"zul"
];
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))();
}
}
}
}
const HIDE_LMS_UI_TOKENS = [
"continue",
"previous",
"exit",
"exitAll",
"abandon",
"abandonAll",
"suspendAll"
];
var RuleConditionOperator = /* @__PURE__ */ ((RuleConditionOperator2) => {
RuleConditionOperator2["NOT"] = "not";
RuleConditionOperator2["AND"] = "and";
RuleConditionOperator2["OR"] = "or";
return RuleConditionOperator2;
})(RuleConditionOperator || {});
var RuleActionType = /* @__PURE__ */ ((RuleActionType2) => {
RuleActionType2["SKIP"] = "skip";
RuleActionType2["DISABLED"] = "disabled";
RuleActionType2["HIDE_FROM_CHOICE"] = "hiddenFromChoice";
RuleActionType2["STOP_FORWARD_TRAVERSAL"] = "stopForwardTraversal";
RuleActionType2["EXIT_PARENT"] = "exitParent";
RuleActionType2["EXIT_ALL"] = "exitAll";
RuleActionType2["RETRY"] = "retry";
RuleActionType2["RETRY_ALL"] = "retryAll";
RuleActionType2["CONTINUE"] = "continue";
RuleActionType2["PREVIOUS"] = "previous";
RuleActionType2["EXIT"] = "exit";
return RuleActionType2;
})(RuleActionType || {});
class RuleCondition extends BaseCMI {
_condition = "always" /* ALWAYS */;
_operator = null;
_parameters = /* @__PURE__ */ new Map();
_referencedObjective = null;
// Optional, overridable provider for current time (LMS may set via SequencingService)
static _now = () => /* @__PURE__ */ new Date();
// Optional, overridable hook for getting elapsed seconds
static _getElapsedSecondsHook = void 0;
/**
* Constructor for RuleCondition
* @param {RuleConditionType} condition - The condition type
* @param {RuleConditionOperator | null} operator - The operator (null for no operator)
* @param {Map<string, any>} parameters - Additional parameters for the condition
*/
constructor(condition = "always" /* ALWAYS */, operator = null, parameters = /* @__PURE__ */ new Map()) {
super("ruleCondition");
this._condition = condition;
this._operator = operator;
this._parameters = parameters;
}
/**
* Allow integrators to override the clock used for time-based rules.
*/
static setNowProvider(now) {
if (typeof now === "function") {
RuleCondition._now = now;
}
}
/**
* Allow integrators to set an elapsed seconds hook for time limit calculations
*/
static setElapsedSecondsHook(hook) {
RuleCondition._getElapsedSecondsHook = hook;
}
/**
* Called when the API needs to be reset
*/
reset() {
this._initialized = false;
this._condition = "always" /* ALWAYS */;
this._operator = null;
this._parameters = /* @__PURE__ */ new Map();
}
/**
* Getter for condition
* @return {RuleConditionType}
*/
get condition() {
return this._condition;
}
/**
* Setter for condition
* @param {RuleConditionType} condition
*/
set condition(condition) {
this._condition = condition;
}
/**
* Getter for operator
* @return {RuleConditionOperator | null}
*/
get operator() {
return this._operator;
}
/**
* Setter for operator
* @param {RuleConditionOperator | null} operator
*/
set operator(operator) {
this._operator = operator;
}
/**
* Getter for parameters
* @return {Map<string, any>}
*/
get parameters() {
return this._parameters;
}
/**
* Setter for parameters
* @param {Map<string, any>} parameters
*/
set parameters(parameters) {
this._parameters = parameters;
}
get referencedObjective() {
return this._referencedObjective;
}
set referencedObjective(objectiveId) {
this._referencedObjective = objectiveId;
}
resolveReferencedObjective(activity) {
if (!this._referencedObjective) {
return null;
}
if (activity.primaryObjective?.id === this._referencedObjective) {
return activity.primaryObjective;
}
const objectives = activity.objectives || [];
return objectives.find((obj) => obj.id === this._referencedObjective) || null;
}
/**
* Evaluate the condition for an activity
* @param {Activity} activity - The activity to evaluate the condition for
* @return {boolean} - True if the condition is met, false otherwise
*/
evaluate(activity) {
let result;
const referencedObjective = this.resolveReferencedObjective(activity);
switch (this._condition) {
case "satisfied" /* SATISFIED */:
case "objectiveSatisfied" /* OBJECTIVE_SATISFIED */:
if (referencedObjective) {
result = referencedObjective.satisfiedStatus === true;
} else {
result = activity.successStatus === SuccessStatus.PASSED || activity.objectiveSatisfiedStatus === true;
}
break;
case "objectiveStatusKnown" /* OBJECTIVE_STATUS_KNOWN */:
result = referencedObjective ? !!referencedObjective.measureStatus : !!activity.objectiveMeasureStatus;
break;
case "objectiveMeasureKnown" /* OBJECTIVE_MEASURE_KNOWN */:
result = referencedObjective ? !!referencedObjective.measureStatus : !!activity.objectiveMeasureStatus;
break;
case "objectiveMeasureGreaterThan" /* OBJECTIVE_MEASURE_GREATER_THAN */: {
const greaterThanValue = this._parameters.get("threshold") || 0;
const measureStatus = referencedObjective ? referencedObjective.measureStatus : activity.objectiveMeasureStatus;
const measureValue = referencedObjective ? referencedObjective.normalizedMeasure : activity.objectiveNormalizedMeasure;
result = !!measureStatus && measureValue > greaterThanValue;
break;
}
case "objectiveMeasureLessThan" /* OBJECTIVE_MEASURE_LESS_THAN */: {
const lessThanValue = this._parameters.get("threshold") || 0;
const measureStatus = referencedObjective ? referencedObjective.measureStatus : activity.objectiveMeasureStatus;
const measureValue = referencedObjective ? referencedObjective.normalizedMeasure : activity.objectiveNormalizedMeasure;
result = !!measureStatus && measureValue < lessThanValue;
break;
}
case "completed" /* COMPLETED */:
case "activityCompleted" /* ACTIVITY_COMPLETED */:
if (referencedObjective) {
result = referencedObjective.completionStatus === CompletionStatus.COMPLETED;
} else {
result = activity.isCompleted;
}
break;
case "progressKnown" /* PROGRESS_KNOWN */:
case "activityProgressKnown" /* ACTIVITY_PROGRESS_KNOWN */:
if (referencedObjective) {
result = referencedObjective.completionStatus !== CompletionStatus.UNKNOWN;
} else {
result = activity.completionStatus !== "unknown";
}
break;
case "attempted" /* ATTEMPTED */:
result = activity.attemptCount > 0;
break;
case "attemptLimitExceeded" /* ATTEMPT_LIMIT_EXCEEDED */:
result = activity.hasAttemptLimitExceeded();
break;
case "timeLimitExceeded" /* TIME_LIMIT_EXCEEDED */:
result = this.evaluateTimeLimitExceeded(activity);
break;
case "outsideAvailableTimeRange" /* OUTSIDE_AVAILABLE_TIME_RANGE */:
result = this.evaluateOutsideAvailableTimeRange(activity);
break;
case "always" /* ALWAYS */:
result = true;
break;
case "never" /* NEVER */:
result = false;
break;
default:
result = false;
break;
}
if (this._operator === "not" /* NOT */) {
result = !result;
}
return result;
}
/**
* Evaluate if time limit has been exceeded
* @param {Activity} activity - The activity to evaluate
* @return {boolean}
* @private
*/
evaluateTimeLimitExceeded(activity) {
let limit = activity.timeLimitDuration;
if (!limit && activity.attemptAbsoluteDurationLimit) {
limit = activity.attemptAbsoluteDurationLimit;
}
if (!limit) {
return false;
}
const limitSeconds = getDurationAsSeconds(limit, scorm2004_regex.CMITimespan);
if (limitSeconds <= 0) {
return false;
}
let elapsedSeconds = 0;
if (RuleCondition._getElapsedSecondsHook) {
try {
const hookResult = RuleCondition._getElapsedSecondsHook(activity);
if (typeof hookResult === "number" && !Number.isNaN(hookResult) && hookResult >= 0) {
elapsedSeconds = hookResult;
}
} catch {
elapsedSeconds = 0;
}
}
if (elapsedSeconds === 0 && activity.attemptExperiencedDuration) {
const attemptDurationSeconds = getDurationAsSeconds(
activity.attemptExperiencedDuration,
scorm2004_regex.CMITimespan
);
if (attemptDurationSeconds > 0) {
elapsedSeconds = attemptDurationSeconds;
}
}
if (elapsedSeconds === 0 && activity.attemptAbsoluteStartTime) {
try {
const start = new Date(activity.attemptAbsoluteStartTime).getTime();
const nowMs = RuleCondition._now().getTime();
if (!Number.isNaN(start) && !Number.isNaN(nowMs) && nowMs >= start) {
elapsedSeconds = (nowMs - start) / 1e3;
}
} catch {
elapsedSeconds = 0;
}
}
return elapsedSeconds > limitSeconds;
}
/**
* Evaluate if activity is outside available time range
* @param {Activity} activity - The activity to evaluate
* @return {boolean}
* @private
*/
evaluateOutsideAvailableTimeRange(activity) {
const beginTime = activity.beginTimeLimit;
const endTime = activity.endTimeLimit;
if (!beginTime && !endTime) {
return false;
}
const now = RuleCondition._now();
if (beginTime) {
const beginDate = new Date(beginTime);
if (now < beginDate) {
return true;
}
}
if (endTime) {
const endDate = new Date(endTime);
if (now > endDate) {
return true;
}
}
return false;
}
/**
* Parse ISO 8601 duration to milliseconds
* Uses the standard getDurationAsSeconds utility which supports full ISO 8601 format
* including date components (years, months, weeks, days) and time components (hours, minutes, seconds).
* @param {string} duration - ISO 8601 duration string (e.g., "PT1H30M", "P1D", "P1Y2M3DT4H5M