import { getH5ApiHost, getOriginalLocalStorage } from '@jolibox/common'; import { compare, major } from './version'; import { track, trackPerformance } from '../utils/report'; import { InternalJSModuleEvalError, InternalJSModuleFetchError } from '@jolibox/common'; import { getBoostrapModuleUrl, getImplementModuleUrl } from './index'; import { timeline } from '../events'; import { testMode } from '@/utils/env'; import { trackError } from '@/utils/event-tracker'; getOriginalLocalStorage(); declare global { interface Window { __joliboxLocalStorage__: Storage; } } const LOCAL_STORE_KEY = 'jolibox-sdk-loadermeta'; interface LocalStoreMeta { bootstrap_version?: string; implement_version?: string; timestamp?: number; } const CURRENT_VERSION_STORE: LocalStoreMeta = JSON.parse( window.__joliboxLocalStorage__.getItem(LOCAL_STORE_KEY) ?? '{}' ); const now = Date.now(); const expired = (now: number, prev: number) => now > prev && Math.abs(now - prev) / (1000 * 60 * 60 * 24) >= 7; async function fetchCurrentRemoteScript() { const host = getH5ApiHost(testMode); const path = `${host}/api/fe-configs/js-sdk/version-metadata`; try { const response = await fetch(path, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); const { implement_version, bootstrap_version }: LocalStoreMeta = await response.json(); let currentStore: LocalStoreMeta = { ...CURRENT_VERSION_STORE, timestamp: now }; if (bootstrap_version) { const { bootstrap_version: currentLocalBootstrapVersion } = CURRENT_VERSION_STORE; if (currentLocalBootstrapVersion && compare(currentLocalBootstrapVersion, bootstrap_version) >= 0) { // no-op } else { currentStore = { ...currentStore, bootstrap_version }; } } if (implement_version) { const { implement_version: currentLocalImpelementVersion } = CURRENT_VERSION_STORE; if (currentLocalImpelementVersion && compare(currentLocalImpelementVersion, implement_version) >= 0) { // no-op } else { currentStore = { ...currentStore, implement_version }; } } window.__joliboxLocalStorage__.setItem(LOCAL_STORE_KEY, JSON.stringify(currentStore)); } catch (error) { console.warn('Failed to fetch loader metadata: ', error); } } export const createLoadImplement = (params: { currentVersion: string; currentMajorVersion: number; bootstrapUrl: string; implementUrl: string; }) => { const { currentMajorVersion, currentVersion } = params; let bootstrapUrl = params.bootstrapUrl; let implementUrl = params.implementUrl; function fetchCurrentBootstrapModule(): | { script: string; type: 'localscript' | 'fetch_from_cdn' } | undefined { const { bootstrap_version, timestamp } = CURRENT_VERSION_STORE; if (bootstrap_version && timestamp) { /** * if current SDK major version is same && remote version is larget than current, use latest version */ if ( !expired(now, timestamp) && major(bootstrap_version) == currentMajorVersion && compare(bootstrap_version, currentVersion) >= 0 ) { bootstrapUrl = getBoostrapModuleUrl(bootstrap_version); } } try { const xhr = new XMLHttpRequest(); xhr.open('GET', bootstrapUrl, false); xhr.send(); return { script: xhr.responseText, type: 'fetch_from_cdn' }; } catch (e) { const internalError = new InternalJSModuleFetchError(`Bootstrap module faile to fetch ${e}`); trackError({ err: `${internalError}` }); } } function loadBootstrapModule() { const startToLoad = Date.now(); track('jsSdkBegin', { t: startToLoad }); timeline.startTime = startToLoad; const { script: currentBootstrapModule, type } = fetchCurrentBootstrapModule() ?? {}; if (currentBootstrapModule) { const script = document.createElement('script'); script.type = 'text/javascript'; script.innerHTML = currentBootstrapModule; document.head.appendChild(script); trackPerformance('bootstrapModuleLoaded', Date.now() - startToLoad, { type }); } } // load remote impletation function loadRemoteImplement() { const implStartLoad = Date.now(); const { implement_version, timestamp } = CURRENT_VERSION_STORE; if (implement_version && timestamp) { /** * if current SDK major version is same && remote version is larget than current, use latest version */ if ( !expired(now, timestamp) && major(implement_version) == currentMajorVersion && compare(implement_version, currentVersion) >= 0 ) { implementUrl = getImplementModuleUrl(implement_version); } } const asyncScript = document.createElement('script'); asyncScript.type = 'module'; asyncScript.crossOrigin = 'anonymous'; asyncScript.src = implementUrl; asyncScript.onload = () => { trackPerformance('implementModuleLoaded', Date.now() - implStartLoad, { type: 'fetch_from_cdn' }); }; document.head.appendChild(asyncScript); } return () => { try { loadBootstrapModule(); loadRemoteImplement(); fetchCurrentRemoteScript(); } catch (e) { const internalError = new InternalJSModuleEvalError(`module evaluate error: ${e}`); trackError({ err: `${internalError}` }); } }; };