Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
741868c
Update: Added a '_shouldRetryConnection' flag to decide if LRS connec…
cahirodoherty-learningpool May 16, 2025
ff21c2c
Added missing property
cahirodoherty-learningpool May 16, 2025
75d3765
Added example property
cahirodoherty-learningpool May 16, 2025
8a35f29
Reverting spacing changes in example.json
cahirodoherty-learningpool May 21, 2025
677f6f0
Replacing retry flag with a configurable number of retries '_retryCon…
cahirodoherty-learningpool May 21, 2025
2f4c912
Correcting property settings
cahirodoherty-learningpool May 21, 2025
1151a57
example.json spacing
cahirodoherty-learningpool May 21, 2025
f1d8bd8
Added logging to display retry behaviour
cahirodoherty-learningpool May 21, 2025
fadbd8a
tab spacing on example.json
cahirodoherty-learningpool May 21, 2025
9350aae
tab spacing on example.json
cahirodoherty-learningpool May 21, 2025
c470e78
Initial setting of this.retriesRemaining value
cahirodoherty-learningpool May 21, 2025
5829e0d
Handling async/promise retries
cahirodoherty-learningpool May 21, 2025
e6433a1
Isolating local retry cycles to prevent race conditions altering avai…
cahirodoherty-learningpool Jun 12, 2025
43c325c
Typo fix and fallbacks added
cahirodoherty-learningpool Jun 12, 2025
b15ce82
Version bump for testing
cahirodoherty-learningpool Jun 18, 2025
c2bc0c1
Typo correction
cahirodoherty-learningpool Jun 18, 2025
109ff13
Adding missing property settings
cahirodoherty-learningpool Jun 18, 2025
7a07474
Typo fix
cahirodoherty-learningpool Jun 18, 2025
f9a8b21
Undoing package bump
cahirodoherty-learningpool Jun 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions example.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"change:_isComplete": true
}
},
"_lrsFailureBehaviour": "show"
"_lrsFailureBehaviour": "show",
"_retryConnectionAttempts": 5
}
}
}
5 changes: 2 additions & 3 deletions js/CMI5.js
Original file line number Diff line number Diff line change
Expand Up @@ -504,9 +504,8 @@ class CMI5 extends Backbone.Controller {
* @param {string} returnURL - The URL to redirect to after exiting the course.
*/
exitCourse(returnURL) {
if (!returnURL) {
return;
}
if (!returnURL) return;

window.location.href = returnURL;
}
}
Expand Down
83 changes: 63 additions & 20 deletions js/XAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class XAPI extends Backbone.Model {
}

/** Implementation starts here */
async initialize() {
async initialize(retriesRemaining = this.getConfig('_retryConnectionAttempts') || 0) {
if (!this.getConfig('_isEnabled')) return this;
if (this.getConfig('_specification') === 'cmi5') {
this.cmi5 = new CMI5(this);
Expand All @@ -83,7 +83,7 @@ class XAPI extends Backbone.Model {
try {
await this.initializeWrapper();
} catch (error) {
this.onInitialised(error);
this.onInitialised(error, retriesRemaining);
return this;
}

Expand Down Expand Up @@ -111,7 +111,7 @@ class XAPI extends Backbone.Model {
if (!this.validateProps()) {
const error = new Error('Missing required properties');
logging.error('adapt-contrib-xapi: xAPI Wrapper initialisation failed', error);
this.onInitialised(error);
this.onInitialised(error, retriesRemaining);
return this;
}

Expand All @@ -135,7 +135,7 @@ class XAPI extends Backbone.Model {
try {
await this.sendStatements(statements);
} catch (error) {
this.onInitialised(error);
this.onInitialised(error, retriesRemaining);
return this;
}

Expand All @@ -155,7 +155,7 @@ class XAPI extends Backbone.Model {
try {
await this.getState();
} catch (error) {
this.onInitialised(error);
this.onInitialised(error, retriesRemaining);
return this;
}

Expand Down Expand Up @@ -258,13 +258,19 @@ class XAPI extends Backbone.Model {
/**
* Triggers 'plugin:endWait' event (if required).
*/
onInitialised(error) {
onInitialised(error, retriesRemaining) {
this.set({ isInitialised: !error });

wait.end();

_.defer(() => {
if (error) {
if (retriesRemaining > 0) {
logging.error('adapt-contrib-xapi: xAPI Wrapper initialisation failed. Retrying...');
this.initialize(retriesRemaining - 1);
return;
}

Adapt.trigger('xapi:lrs:initialize:error', error);
return;
}
Expand Down Expand Up @@ -442,9 +448,7 @@ class XAPI extends Backbone.Model {
// If cmi5 and
// the launch mode is not normal (but either Review or Browse)
// THEN do not listen to cmi5 defined statements
if (this.cmi5 && this.get('launchData')?.launchMode !== 'Normal') {
return;
}
if (this.cmi5 && this.get('launchData')?.launchMode !== 'Normal') return;

// Allow surfacing the learner's info in _globals.
this.getLearnerInfo();
Expand Down Expand Up @@ -1138,17 +1142,41 @@ class XAPI extends Backbone.Model {
for (const collectionName of changedCollectionNames) {
const newState = this.get('state')[collectionName];

await new Promise(resolve => {
this.xapiWrapper.sendState(activityId, actor, collectionName, registration, newState, null, null, (error, xhr) => {
if (error) {
Adapt.trigger('xapi:lrs:sendState:error', error);
return resolve();
}
const retriesRemaining = this.getConfig('_retryConnectionAttempts') || 0;

while (retriesRemaining > 0) {
const result = await new Promise(resolve => {
this.xapiWrapper.sendState(
activityId,
actor,
collectionName,
registration,
newState,
null,
null,
(error, xhr) => {
if (error) {
logging.error('adapt-contrib-xapi: xAPI sendStateToServer failed. Retrying...');
return resolve({ success: false, error });
}

Adapt.trigger('xapi:lrs:sendState:success', newState);
return resolve();
Adapt.trigger('xapi:lrs:sendState:success', newState);
return resolve({ success: true });
}
);
});
});

if (result.success) {
break;
} else {
// Last retry attempt has just been performed
if (retriesRemaining === 1) {
Adapt.trigger('xapi:lrs:sendState:error', result.error);
}

retriesRemaining--;
}
}
}
}

Expand Down Expand Up @@ -1395,7 +1423,7 @@ class XAPI extends Backbone.Model {
* feature not available in AJAX requests. This makes the sending of suspended
* and terminated statements more reliable.
*/
async sendStatementsSync(statements) {
async sendStatementsSync(statements, retriesRemaining = this.getConfig('_retryConnectionAttempts') || 0) {
const lrs = window.ADL.XAPIWrapper.lrs;

// Fetch not supported in IE and keepalive/custom headers
Expand Down Expand Up @@ -1435,9 +1463,16 @@ class XAPI extends Backbone.Model {
method: 'POST'
});
} catch (error) {
if (retriesRemaining > 0) {
logging.error('adapt-contrib-xapi: xAPI sendStatementsSync failed. Retrying...');
this.sendStatementsSync(statements, retriesRemaining - 1);
return;
}

Adapt.trigger('xapi:lrs:sendStatement:error', error);
return;
}

Adapt.trigger('xapi:lrs:sendStatement:success', statements);
}

Expand All @@ -1460,16 +1495,24 @@ class XAPI extends Backbone.Model {
* Send an xAPI statement to the LRS once all async operations are complete
* @param {ADL.XAPIStatement} statement - A valid ADL.XAPIStatement object.
* @param {array} [attachments] - An array of attachments to pass to the LRS.
* @param {int} retriesRemaining - The number of times to attempt retry of function on failure.
*/
async onStatementReady(statement, attachments) {
async onStatementReady(statement, attachments, retriesRemaining = this.getConfig('_retryConnectionAttempts') || 0) {
const sendStatementCallback = (error, res, body) => {
if (error) {
if (retriesRemaining > 0) {
logging.error('adapt-contrib-xapi: xAPI sendStatement failed. Retrying...');
this.onStatementReady(statement, attachments, retriesRemaining - 1);
return;
}

Adapt.trigger('xapi:lrs:sendStatement:error', error);
throw error;
}

Adapt.trigger('xapi:lrs:sendStatement:success', body);
};

if (this.cmi5) {
this.cmi5.mergeDefaultContext(statement);
}
Expand Down
4 changes: 1 addition & 3 deletions js/XAPIIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ class XAPIIndex extends Backbone.Controller {
async onDataLoaded() {
const config = Adapt.config.get('_xapi') || {};

if (!config._isEnabled) {
return;
}
if (!config._isEnabled) return;

const xapi = await XAPI.getInstance();

Expand Down
13 changes: 12 additions & 1 deletion properties.schema
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@
},
"_auID": {
"type": "string",
"title": "assignable unit (AU) ID",
"title": "Assignable Unit (AU) ID",
"default": "1",
"inputType": "Text",
"validators": [],
"help": "Unique identifier for this assignable unit."
},
"_endpoint": {
Expand Down Expand Up @@ -310,6 +312,15 @@
},
"validators": [],
"help": "Determines how the plugin should behave whenever it fails to successfully connect or send statements to the configured LRS"
},
"_retryConnectionAttempts": {
"type": "number",
"required": true,
"default": 0,
"title": "How many attempts should be made to reestablish an LRS connection if disconnected?",
"inputType": "Number",
"validators": ["required", "number"],
"help": "Indicates how many attempts this course should make to retry initialization or sending state/statements to a configured Learning Record Store (LRS) after a failure."
}
}
}
Expand Down