diff --git a/example.json b/example.json index 0d233e2..9e26f4d 100644 --- a/example.json +++ b/example.json @@ -30,6 +30,7 @@ "change:_isComplete": true } }, - "_lrsFailureBehaviour": "show" + "_lrsFailureBehaviour": "show", + "_retryConnectionAttempts": 5 } -} \ No newline at end of file +} diff --git a/js/CMI5.js b/js/CMI5.js index 925055b..4a8e6ae 100644 --- a/js/CMI5.js +++ b/js/CMI5.js @@ -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; } } diff --git a/js/XAPI.js b/js/XAPI.js index 46c938d..f792925 100644 --- a/js/XAPI.js +++ b/js/XAPI.js @@ -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); @@ -83,7 +83,7 @@ class XAPI extends Backbone.Model { try { await this.initializeWrapper(); } catch (error) { - this.onInitialised(error); + this.onInitialised(error, retriesRemaining); return this; } @@ -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; } @@ -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; } @@ -155,7 +155,7 @@ class XAPI extends Backbone.Model { try { await this.getState(); } catch (error) { - this.onInitialised(error); + this.onInitialised(error, retriesRemaining); return this; } @@ -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; } @@ -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(); @@ -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--; + } + } } } @@ -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 @@ -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); } @@ -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); } diff --git a/js/XAPIIndex.js b/js/XAPIIndex.js index 586ae9a..1fee1fe 100644 --- a/js/XAPIIndex.js +++ b/js/XAPIIndex.js @@ -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(); diff --git a/properties.schema b/properties.schema index 7817e24..bd33d8c 100755 --- a/properties.schema +++ b/properties.schema @@ -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": { @@ -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." } } }