@rusticisoftware/cmi5
Version:
A JavaScript library implementing the cmi5 specification for AUs during runtime.
1 lines • 19.8 kB
JavaScript
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Cmi5=e():t.Cmi5=e()}(self,(()=>(()=>{"use strict";var t={d:(e,n)=>{for(var r in n)t.o(n,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},e={};t.d(e,{default:()=>v});const n="3.1.0",r="@rusticisoftware/cmi5",i="1.0.3",o="cmi5LearnerPreferences",a="Normal",s={id:"https://w3id.org/xapi/cmi5/context/categories/cmi5"},c={id:"https://w3id.org/xapi/cmi5/context/categories/moveon"},l={id:`http://id.tincanapi.com/activity/software/${r}/${n}`,definition:{name:{und:`${r} (${n})`},description:{en:"A JavaScript library implementing the cmi5 specification for AUs during runtime."},type:"http://id.tincanapi.com/activitytype/source"}},h="https://w3id.org/xapi/cmi5/context/extensions/masteryscore",u="http://adlnet.gov/expapi/verbs/initialized",d="http://adlnet.gov/expapi/verbs/terminated",f="http://adlnet.gov/expapi/verbs/completed",m="http://adlnet.gov/expapi/verbs/passed",p="http://adlnet.gov/expapi/verbs/failed",g={[u]:{en:"initialized"},[d]:{en:"terminated"},[f]:{en:"completed"},[m]:{en:"passed"},[p]:{en:"failed"}},w=["endpoint","fetch","actor","activityId","registration"];function v(t){if(this.log("constructor",t),void 0!==t){const e=new URL(t).searchParams;this.log("params");for(let t=0;t<w.length;t+=1)if(!e.has(w[t]))throw new Error(`Invalid launch string missing or empty parameter: ${w[t]}`);this.setFetch(e.get("fetch")),this.setEndpoint(e.get("endpoint")),this.setActor(e.get("actor")),this.setActivityId(e.get("activityId")),this.setRegistration(e.get("registration"))}}return v.VERSION=n,v.DEBUG=!1,v.enableDebug=()=>{v.DEBUG=!0},v.disableDebug=()=>{v.DEBUG=!1},v.uuidv4=()=>([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16))),v.convertISO8601DurationToMilliseconds=t=>{const e=t.indexOf("-")>=0,n=t.indexOf("T"),r=t.indexOf("S");let i,o,a=t.indexOf("H"),s=t.indexOf("M");if(-1===n||-1!==s&&s<n||-1!==t.indexOf("D")||-1!==t.indexOf("Y"))throw new Error("ISO 8601 durations including years, months and/or days are not currently supported");-1===a?(a=n,i=0):i=parseInt(t.slice(n+1,a),10),-1===s?(s=n,o=0):o=parseInt(t.slice(a+1,s),10);const c=parseFloat(t.slice(s+1,r));let l=parseInt(1e3*(60*(60*i+o)+c),10);return isNaN(l)&&(l=0),e&&(l*=-1),l},v.convertMillisecondsToISO8601Duration=t=>{const e=parseInt(t,10);let n="PT",r=Math.round(e/10);r<0&&(n="-"+n,r*=-1);const i=parseInt(r/36e4,10),o=parseInt(r%36e4/6e3,10);return i>0&&(n+=i+"H"),o>0&&(n+=o+"M"),n+=r%36e4%6e3/100+"S",n},v.prototype={_fetch:null,_endpoint:null,_actor:null,_registration:null,_activityId:null,_auth:null,_fetchContent:null,_lmsLaunchData:null,_contextTemplate:null,_learnerPrefs:null,_isActive:!1,_initialized:null,_passed:null,_failed:null,_completed:null,_terminated:null,_durationStart:null,_progress:null,_includeSourceActivity:!0,start:async function(t={},e){this.log("start");try{await this.postFetch(),void 0!==t.postFetch&&await t.postFetch.apply(this),await this.loadLMSLaunchData(),void 0!==t.launchData&&await t.launchData.apply(this),await this.loadLearnerPrefs(),void 0!==t.learnerPrefs&&await t.learnerPrefs.apply(this),await this.initialize(e),void 0!==t.initializeStatement&&await t.initializeStatement.apply(this)}catch(t){throw new Error(`Failed to start AU: ${t}`)}},postFetch:async function(){if(this.log("postFetch"),null===this._fetch)throw new Error("Can't POST to fetch URL without setFetch");let t;try{t=await fetch(this._fetch,{mode:"cors",method:"POST"})}catch(t){throw new Error(`Failed to make fetch request: ${t}`)}if(!t.ok)throw new Error(`Post fetch response returned error: ${t.status}`);if(200===t.status){const e=await t.json();if(void 0!==e["auth-token"])return this._fetchContent=e,void this.setAuth(`Basic ${e["auth-token"]}`);throw new Error(`Post fetch response indicated LMS error: ${e["error-code"]}`)}throw new Error(`Post fetch response status code unexpected: ${t.status}`)},loadLMSLaunchData:async function(){if(this.log("loadLMSLaunchData"),null===this._fetchContent)throw new Error("Can't retrieve LMS Launch Data without successful postFetch");let t;try{t=await fetch(`${this._endpoint}/activities/state?`+new URLSearchParams({stateId:"LMS.LaunchData",activityId:this._activityId,agent:JSON.stringify(this._actor),registration:this._registration}),{mode:"cors",method:"get",headers:{"X-Experience-API-Version":i,Authorization:this._auth}})}catch(t){throw new Error(`Failed to GET LMS launch data: ${t}`)}this._lmsLaunchData=await t.json(),this._contextTemplate=JSON.stringify(this._lmsLaunchData.contextTemplate)},loadLearnerPrefs:async function(){if(this.log("loadLearnerPrefs"),null===this._lmsLaunchData)throw new Error("Can't retrieve Learner Preferences without successful loadLMSLaunchData");let t;try{t=await fetch(`${this._endpoint}/agents/profile?`+new URLSearchParams({profileId:o,agent:JSON.stringify(this._actor)}),{mode:"cors",method:"get",headers:{"X-Experience-API-Version":i,Authorization:this._auth}})}catch(t){throw new Error(`Failed request to retrieve learner preferences: ${t}`)}if(200!==t.status){if(404===t.status)return this.log("Learner Preferences request returned not found (expected)"),void(this._learnerPrefs={contents:{}});throw new Error("Failed to get learner preferences: unrecognized response status")}this._learnerPrefs={contents:await t.json(),etag:t.headers.get("ETag")}},saveLearnerPrefs:async function(){if(this.log("saveLearnerPrefs"),null===this._learnerPrefs)throw new Error("Can't save Learner Preferences without first loading them");let t;try{t=await fetch(`${this._endpoint}/agents/profile?`+new URLSearchParams({profileId:o,agent:JSON.stringify(this._actor)}),{mode:"cors",method:"put",headers:{"X-Experience-API-Version":i,Authorization:this._auth,"Content-Type":"application/json",...this._learnerPrefs.etag?{"If-Match":this._learnerPrefs.etag}:{"If-None-Match":"*"}},body:JSON.stringify(this._learnerPrefs.contents)})}catch(t){throw new Error(`Failed request to save learner preferences: ${t}`)}if(403===t.status)return this.log("Save of learner preferences denied by LMS"),void(this._learnerPrefs.saveDisallowed=!0);if(204!==t.status)throw new Error(`Failed to save learner preferences: ${t.status}`);this._learnerPrefs.etag=t.headers.get("ETag")},initialize:async function(t={}){if(this.log("initialize"),null===this._lmsLaunchData)throw new Error("Failed to initialize: can't send initialized statement without successful loadLMSLaunchData");if(null===this._learnerPrefs)throw new Error("Failed to initialize: can't send initialized statement without successful loadLearnerPrefs");if(this._initialized)throw new Error("Failed to initialize: AU already initialized");const e=this.initializedStatement();this._appendProvidedProperties(e,t);try{await this.sendStatement(e)}catch(t){throw new Error(`Failed to initialize: exception sending initialized statement (${t})`)}return this._initialized=!0,this._isActive=!0,this._durationStart=(new Date).getTime(),e},terminate:async function(t={}){if(this.log("terminate"),!this._initialized)throw new Error("AU not initialized");if(this._terminated)throw new Error("AU already terminated");const e=this.terminatedStatement();this._appendProvidedProperties(e,t);try{await this.sendStatement(e)}catch(t){throw new Error(`Failed to terminate: exception sending terminated statement (${t})`)}return this._terminated=!0,this._isActive=!1,e},completed:async function(t={}){if(this.log("completed"),!this.isActive())throw new Error("AU not active");if(this.getLaunchMode()!==a)throw new Error("AU not in Normal launch mode");if(this._completed)throw new Error("AU already completed");const e=this.completedStatement();this._appendProvidedProperties(e,t);try{await this.sendStatement(e)}catch(t){throw new Error(`Failed to send completed statement: ${t}`)}return this.setProgress(null),this._completed=!0,e},passed:async function(t){if(this.log("passed"),!this.isActive())throw new Error("AU not active");if(this.getLaunchMode()!==a)throw new Error("AU not in Normal launch mode");if(null!==this._passed)throw new Error("AU already passed");let e;try{e=this.passedStatement(t)}catch(t){throw new Error(`Failed to create passed statement: ${t}`)}try{await this.sendStatement(e)}catch(t){throw new Error(`Failed to send passed statement: ${t}`)}return this._passed=!0,e},failed:async function(t){if(this.log("failed"),!this.isActive())throw new Error("AU not active");if(this.getLaunchMode()!==a)throw new Error("AU not in Normal launch mode");if(null!==this._failed||null!==this._passed)throw new Error("AU already passed/failed");let e;try{e=this.failedStatement(t)}catch(t){throw new Error(`Failed to create failed statement: ${t}`)}try{await this.sendStatement(e)}catch(t){throw new Error(`Failed to send failed statement: ${t}`)}return this._failed=!0,e},isActive:function(){return this.log("isActive"),this._isActive},log:function(){v.DEBUG&&"undefined"!=typeof console&&console.log&&(arguments[0]="cmi5.js:"+arguments[0],console.log.apply(console,arguments))},includeSourceActivity:function(t){this._includeSourceActivity=!!t},getLaunchMode:function(){if(this.log("getLaunchMode"),null===this._lmsLaunchData)throw new Error("Can't determine launchMode until LMS LaunchData has been loaded");return this._lmsLaunchData.launchMode},getLaunchParameters:function(){if(this.log("getLaunchParameters"),null===this._lmsLaunchData)throw new Error("Can't determine LaunchParameters until LMS LaunchData has been loaded");let t=null;return void 0!==this._lmsLaunchData.launchParameters&&(t=this._lmsLaunchData.launchParameters),t},getSessionId:function(){if(this.log("getSessionId"),null===this._lmsLaunchData)throw new Error("Can't determine session id until LMS LaunchData has been loaded");return this._lmsLaunchData.contextTemplate.extensions["https://w3id.org/xapi/cmi5/context/extensions/sessionid"]},getMoveOn:function(){if(this.log("getMoveOn"),null===this._lmsLaunchData)throw new Error("Can't determine moveOn until LMS LaunchData has been loaded");return this._lmsLaunchData.moveOn},getMasteryScore:function(){if(this.log("getMasteryScore"),null===this._lmsLaunchData)throw new Error("Can't determine masteryScore until LMS LaunchData has been loaded");let t=null;return void 0!==this._lmsLaunchData.masteryScore&&(t=this._lmsLaunchData.masteryScore),t},getReturnURL:function(){if(this.log("getReturnURL"),null===this._lmsLaunchData)throw new Error("Can't determine returnURL until LMS LaunchData has been loaded");let t=null;return void 0!==this._lmsLaunchData.returnURL&&(t=this._lmsLaunchData.returnURL),t},getEntitlementKey:function(){if(this.log("getEntitlementKey"),null===this._lmsLaunchData)throw new Error("Can't determine entitlementKey until LMS LaunchData has been loaded");let t=null;return void 0!==this._lmsLaunchData.entitlementKey&&(void 0!==this._lmsLaunchData.entitlementKey.alternate?t=this._lmsLaunchData.entitlementKey.alternate:void 0!==this._lmsLaunchData.entitlementKey.courseStructure&&(t=this._lmsLaunchData.entitlementKey.courseStructure)),t},getLanguagePreference:function(){if(this.log("getLanguagePreference"),null===this._learnerPrefs)throw new Error("Can't determine language preference until learner preferences have been loaded");let t=null;return void 0!==this._learnerPrefs.contents.languagePreference&&(t=this._learnerPrefs.contents.languagePreference),t},setLanguagePreference:function(t){if(this.log("setLanguagePreference"),null===this._learnerPrefs)throw new Error("Can't set language preference until learner preferences have been loaded");""===t&&(t=null),this._learnerPrefs.contents.languagePreference=t},getAudioPreference:function(){if(this.log("getAudioPreference"),null===this._learnerPrefs)throw new Error("Can't determine audio preference until learner preferences have been loaded");let t=null;return void 0!==this._learnerPrefs.contents.audioPreference&&(t=this._learnerPrefs.contents.audioPreference),t},setAudioPreference:function(t){if(this.log("setAudioPreference"),null===this._learnerPrefs)throw new Error("Can't set audio preference until learner preferences have been loaded");if("on"!==t&&"off"!==t&&null!==t)throw new Error(`Unrecognized value for audio preference: ${t}`);this._learnerPrefs.contents.audioPreference=t},getDuration:function(){return this.log("getDuration"),(new Date).getTime()-this._durationStart},setProgress:function(t){if(this.log("setProgress: ",t),null!==t){if(!Number.isInteger(t))throw new Error(`Invalid progress measure (not an integer): ${t}`);if(t<0||t>100)throw new Error(`Invalid progress measure must be greater than or equal to 0 and less than or equal to 100: ${t}`)}this._progress=t},getProgress:function(){return this.log("getProgress"),this._progress},setEndpoint:function(t){this.log("setEndpoint: ",t),this._endpoint=t,this._endpoint=this._endpoint.replace(/\/+$/,"")},getEndpoint:function(){return this._endpoint},setAuth:function(t){this.log("setAuth: ",t),this._auth=t},getAuth:function(){return this._auth},setFetch:function(t){this.log("setFetch: ",t),this._fetch=t},getFetch:function(){return this._fetch},setActor:function(t){if(this.log("setActor",t),"string"==typeof t)try{t=JSON.parse(t)}catch(t){throw new Error(`Invalid actor: failed to parse string as JSON (${t})`)}if(void 0===t.account)throw new Error("Invalid actor: account is missing");if(void 0===t.account.name)throw new Error("Invalid actor: account name is missing");if(""===t.account.name)throw new Error("Invalid actor: account name is empty");if(void 0===t.account.homePage)throw new Error("Invalid actor: account homePage is missing");if(""===t.account.homePage)throw new Error("Invalid actor: account homePage is empty");this._actor=t},getActor:function(){return this._actor},setActivityId:function(t){if(this.log("setActivityId",t),void 0===t)throw new Error("Invalid activityId: argument missing");if(""===t)throw new Error("Invalid activityId: empty string");this._activityId=t},getActivityId:function(){return this._activityId},setRegistration:function(t){if(this.log("setRegistration",t),null===t)throw new Error("Invalid registration: null");if(""===t)throw new Error("Invalid registration: empty");this._registration=t},getRegistration:function(){return this._registration},validateScore:function(t){if(null==t)throw new Error(`cannot validate score (score not provided): ${t}`);if(void 0!==t.min&&!Number.isInteger(t.min))throw new Error("score.min is not an integer");if(void 0!==t.max&&!Number.isInteger(t.max))throw new Error("score.max is not an integer");if(void 0!==t.scaled){if(!/^(-|\+)?[01]+(\.[0-9]+)?$/.test(t.scaled))throw new Error(`scaled score not a recognized number: ${t.scaled}`);if(t.scaled<0)throw new Error("scaled score must be greater than or equal to 0");if(t.scaled>1)throw new Error("scaled score must be less than or equal to 1")}if(void 0!==t.raw){if(!Number.isInteger(t.raw))throw new Error("score.raw is not an integer");if(void 0===t.min)throw new Error("minimum score must be provided when including a raw score");if(void 0===t.max)throw new Error("maximum score must be provided when including a raw score");if(t.raw<t.min)throw new Error("raw score must be greater than or equal to minimum score");if(t.raw>t.max)throw new Error("raw score must be less than or equal to maximum score")}return!0},prepareStatement:function(t){const e={id:v.uuidv4(),timestamp:(new Date).toISOString(),actor:this._actor,verb:{id:t},object:{id:this._activityId},context:this._prepareContext()},n=this.getProgress();return void 0!==g[t]&&(e.verb.display=g[t]),t!==f&&null!==n&&(e.result={extensions:{"https://w3id.org/xapi/cmi5/result/extensions/progress":n}}),e},sendStatement:async function(t){let e;this.log("sendStatement",t),void 0===t.id&&(t.id=v.uuidv4());try{e=await fetch(`${this._endpoint}/statements?`+new URLSearchParams({statementId:t.id}),{mode:"cors",method:"put",headers:{"X-Experience-API-Version":i,Authorization:this._auth,"Content-Type":"application/json"},body:JSON.stringify(t)})}catch(t){throw new Error(`Failed request to send statement: ${t}`)}if(204!==e.status)throw new Error(`Failed to send statement: status code ${e.status}`)},sendStatements:async function(t){let e;this.log("sendStatements",t),t.forEach((t=>{void 0===t.id&&(t.id=v.uuidv4())}));try{e=await fetch(`${this._endpoint}/statements`,{mode:"cors",method:"post",headers:{"X-Experience-API-Version":i,Authorization:this._auth,"Content-Type":"application/json"},body:JSON.stringify(t)})}catch(t){throw new Error(`Failed request to send statements: ${t}`)}if(200!==e.status)throw new Error(`Failed to send statements: status code ${e.status}`)},initializedStatement:function(){return this.log("initializedStatement"),this._prepareStatement(u)},terminatedStatement:function(){this.log("terminatedStatement");const t=this._prepareStatement(d);return t.result=t.result||{},t.result.duration=v.convertMillisecondsToISO8601Duration(this.getDuration()),t},passedStatement:function(t){this.log("passedStatement");const e=this._prepareStatement(m);if(e.result=e.result||{},e.result.success=!0,e.result.duration=v.convertMillisecondsToISO8601Duration(this.getDuration()),t){try{this.validateScore(t)}catch(t){throw new Error(`Invalid score: ${t}`)}const n=this.getMasteryScore();if(null!==n&&void 0!==t.scaled){if(t.scaled<n)throw new Error(`Invalid score: scaled score does not meet or exceed mastery score (${t.scaled} < ${n})`);e.context.extensions=e.context.extensions||{},e.context.extensions[h]=n}e.result.score=t}return e.context.contextActivities.category.push(c),e},failedStatement:function(t){this.log("failedStatement");const e=this._prepareStatement(p);if(e.result=e.result||{},e.result.success=!1,e.result.duration=v.convertMillisecondsToISO8601Duration(this.getDuration()),t){try{this.validateScore(t)}catch(t){throw new Error(`Invalid score: ${t}`)}const n=this.getMasteryScore();if(null!==n&&void 0!==t.scaled){if(t.scaled>=n)throw new Error(`Invalid score: scaled score exceeds mastery score (${t.scaled} >= ${n})`);e.context.extensions=e.context.extensions||{},e.context.extensions[h]=n}e.result.score=t}return e.context.contextActivities.category.push(c),e},completedStatement:function(){this.log("completedStatement");const t=this._prepareStatement(f);return t.result=t.result||{},t.result.completion=!0,t.result.duration=v.convertMillisecondsToISO8601Duration(this.getDuration()),t.context.contextActivities.category.push(c),t},_prepareContext:function(){const t=JSON.parse(this._contextTemplate);return t.registration=this._registration,this._includeSourceActivity&&(t.contextActivities=t.contextActivities||{},t.contextActivities.other=t.contextActivities.other||[],t.contextActivities.other.push(l)),t},_prepareStatement:function(t){const e=this.prepareStatement(t);return e.context.contextActivities=e.context.contextActivities||{},e.context.contextActivities.category=e.context.contextActivities.category||[],e.context.contextActivities.category.push(s),e},_appendProvidedProperties:function(t,e){if(void 0!==e.context&&void 0!==e.context.extensions)for(const n in e.context.extensions)e.context.extensions.hasOwnProperty(n)&&(t.context.extensions[n]=e.context.extensions[n]);if(void 0!==e.result&&(t.result=t.result||{},t.result.extensions=t.result.extensions||{},void 0!==e.result.extensions))for(const n in e.result.extensions)e.result.extensions.hasOwnProperty(n)&&(t.result.extensions[n]=e.result.extensions[n]);void 0!==e.object&&void 0!==e.object.definition&&(t.object.definition=t.object.definition||{},void 0!==e.object.definition.type&&(t.object.definition.type=e.object.definition.type))}},e.default})()));