scorm-again
Version:
A modern SCORM JavaScript run-time library for SCORM 1.2 and SCORM 2004
3 lines (2 loc) • 5.5 kB
JavaScript
class e{_cache=new Map;_cacheTimestamps=new Map;_lastError="0";_pending=new Map;_counter=0;_origin;_targetWindow;_timeout;_heartbeatInterval;_heartbeatTimeout;_destroyed=!1;_connected=!0;_lastHeartbeatResponse=Date.now();_heartbeatTimer;_eventListeners=new Map;_boundOnMessage;static _isValidMessageResponse(e){if("object"!=typeof e||null===e)return!1;if("string"!=typeof e.messageId||0===e.messageId.length)return!1;if(void 0!==e.error){if("object"!=typeof e.error||null===e.error)return!1;const e=e.error;if("string"!=typeof e.message)return!1;if(void 0!==e.code&&"string"!=typeof e.code)return!1}return void 0===e.isHeartbeat||"boolean"==typeof e.isHeartbeat}static _validateArgs(e){return!!Array.isArray(e)}_handler={get:(t,r,s)=>{if("string"!=typeof r||r in t){const e=Reflect.get(t,r,s);return"function"==typeof e?e.bind(t):e}const i=r,a=i.endsWith("GetValue"),n=i.startsWith("LMSSet")||i.endsWith("SetValue"),o="Initialize"===i||"LMSInitialize"===i,c="Terminate"===i||"LMSFinish"===i,h="Commit"===i||"LMSCommit"===i,_="GetErrorString"===i||"LMSGetErrorString"===i,d="GetDiagnostic"===i||"LMSGetDiagnostic"===i;return(...r)=>{if(!e._validateArgs(r))return console.error("CrossFrameAPI: Invalid arguments for "+i),"";if(n&&r.length>=2){const e=r[0];t._cache.set(e,r[1]+""),t._cacheTimestamps.set(e,Date.now()),t._lastError="0"}const s=Date.now();if(t._post(i,r).then(e=>{if(a&&r.length>=1){const i=r[0],a=t._cacheTimestamps.get(i)??0;s>a&&(t._cache.set(i,e+""),t._cacheTimestamps.delete(i)),t._lastError="0"}_&&r.length>=1&&t._cache.set("error_"+r[0],e+""),d&&r.length>=1&&t._cache.set("diag_"+r[0],e+""),"GetLastError"!==i&&"LMSGetLastError"!==i||(t._lastError=e+"")}).catch(e=>t._capture(i,e)),a&&r.length>=1)return t._cache.get(r[0])??"";if(_&&r.length>=1)return t._cache.get("error_"+r[0])??"";if(d&&r.length>=1)return t._cache.get("diag_"+r[0])??"";if(o||c||h||n){const result="true";return t._post("getFlattenedCMI",[]).then(e=>{e&&"object"==typeof e&&Object.entries(e).forEach(([e,r])=>{const i=t._cacheTimestamps.get(e)??0;s>i&&(t._cache.set(e,r),t._cacheTimestamps.delete(e))}),t._lastError="0"}).catch(e=>t._capture("getFlattenedCMI",e)),result}return"GetLastError"===i||"LMSGetLastError"===i?t._lastError:""}}};constructor(e="*",t=window.parent,r={}){return this._origin=e,this._targetWindow=t,this._timeout=r.timeout??5e3,this._heartbeatInterval=r.heartbeatInterval??3e4,this._heartbeatTimeout=r.heartbeatTimeout??6e4,"*"===e&&console.warn("CrossFrameAPI: Using wildcard origin ('*') allows any origin to receive messages. This is insecure for production use. Specify an explicit origin (e.g., 'https://lms.example.com') to restrict message recipients."),this._boundOnMessage=this._onMessage.bind(this),window.addEventListener("message",this._boundOnMessage),this._startHeartbeat(),new Proxy(this,this._handler)}destroy(){if(!this._destroyed){this._destroyed=!0,window.removeEventListener("message",this._boundOnMessage),this._heartbeatTimer&&(clearInterval(this._heartbeatTimer),this._heartbeatTimer=void 0);for(const e of Array.from(this._pending.values()))clearTimeout(e.timer),e.reject(Error("CrossFrameAPI destroyed"));this._pending.clear(),this._cache.clear(),this._cacheTimestamps.clear(),this._eventListeners.clear()}}on(e,t){this._eventListeners.has(e)||this._eventListeners.set(e,new Set),this._eventListeners.get(e)?.add(t)}off(e,t){this._eventListeners.get(e)?.delete(t)}get connected(){return this._connected}_emit(e){this._eventListeners.get(e.type)?.forEach(t=>t(e))}_startHeartbeat(){this._heartbeatTimer&&clearInterval(this._heartbeatTimer),this._heartbeatTimer=setInterval(()=>{this._destroyed||(Date.now()-this._lastHeartbeatResponse>this._heartbeatTimeout&&this._connected&&(this._connected=!1,this._emit({type:"connectionLost"})),this._sendHeartbeat())},this._heartbeatInterval)}_sendHeartbeat(){const e=`hb-${Date.now()}-${this._counter++}`;this._targetWindow.postMessage({messageId:e,method:"__heartbeat__",params:[],isHeartbeat:!0},this._origin)}_post(e,t){if(this._destroyed)return Promise.reject(Error("CrossFrameAPI destroyed"));const r=`cfapi-${Date.now()}-${this._counter++}`,s=Date.now(),i=t.map(t=>{if("function"!=typeof t)return t;console.warn("Dropping function param when posting SCORM call:",e)});return new Promise((t,a)=>{const n=setTimeout(()=>{this._pending.has(r)&&(this._pending.delete(r),a(Error("Timeout calling "+e)))},this._timeout);this._pending.set(r,{resolve:t,reject:a,timer:n,requestTime:s,method:e}),this._targetWindow.postMessage({messageId:r,method:e,params:i},this._origin)})}_onMessage(t){if(this._destroyed)return;if("*"!==this._origin&&t.origin!==this._origin)return;if(t.source&&t.source!==this._targetWindow)return;if(!e._isValidMessageResponse(t.data))return;const r=t.data;if(r.isHeartbeat)return this._lastHeartbeatResponse=Date.now(),void(this._connected||(this._connected=!0,this._emit({type:"connectionRestored"})));const s=this._pending.get(r.messageId);s&&(clearTimeout(s.timer),this._pending.delete(r.messageId),r.error?("Rate limit exceeded"===r.error.message&&this._emit({type:"rateLimited",method:s.method}),s.reject(r.error)):s.resolve(r.result))}_capture(e,t){let r="Unknown error";t instanceof Error?r=t.message:"object"==typeof t&&null!==t&&"message"in t&&(r=t.message+""),console.error(`CrossFrameAPI ${e} error:`,t);const s=/(?:error code|code)?\s*(\d{3})\b/i.exec(r),i=s?.[1]??"101";this._lastError=i,this._cache.set("error_"+i,r)}}export{e as CrossFrameAPI};
//# sourceMappingURL=cross-frame-api.min.js.map