/** * CDN loader for TapSDK * Dynamically loads the TapKit SDK from CDN * * For local testing, you can override the loader URL: * window.__TAP_KIT_LOADER_URL__ = '/tap-kit-core/loader.js'; * * For local development (bypass loader, load IIFE directly): * window.__TAP_KIT_CORE_URL__ = '/packages/tap-kit-core/dist/index.global.js'; */ // Build-time constant injected by tsup define // Production: https://files.edutap.ai/tap-sdk/loader.js // Demo: https://files.edutap.ai/tap-sdk/loader-demo.js declare const __DEFAULT_CDN_LOADER_URL__: string; const DEFAULT_CDN_LOADER_URL = __DEFAULT_CDN_LOADER_URL__; const DEFAULT_TIMEOUT_MS = 4000; // 4 seconds total timeout const IDLE_CALLBACK_TIMEOUT_MS = 500; // 500ms for requestIdleCallback /** * Get the loader URL from window override or default CDN */ function getLoaderURL(): string { return window?.__TAP_KIT_LOADER_URL__ ? window.__TAP_KIT_LOADER_URL__ : DEFAULT_CDN_LOADER_URL; } /** * Check if local core mode is enabled * When __TAP_KIT_CORE_URL__ is set, directly load the IIFE bundle * This bypasses the loader.js and loads tap-kit-core directly */ function isLocalCoreMode(): boolean { return typeof window !== "undefined" && !!window.__TAP_KIT_CORE_URL__; } /** * Get the local core URL */ function getLocalCoreURL(): string { return window.__TAP_KIT_CORE_URL__ || ""; } /** * Creates a SDK checker function with timeout and retry logic * Uses requestIdleCallback to avoid blocking browser rendering * @param resolve - Promise resolve function * @param reject - Promise reject function * @param timeoutMs - Maximum time to wait for SDK to load (milliseconds) * @returns Checker function to be called repeatedly */ function createSDKChecker( resolve: (value: void | PromiseLike) => void, reject: (reason?: any) => void, timeoutMs: number ): () => void { const startTime = Date.now(); const checkSDK = (): void => { // Check if real TapKit is loaded (not just stub) // Stub has TapKitLoaded flag set to true by loader.js after real SDK loads if (window.TapKit && window.TapKitLoaded === true) { window.__TAP_KIT_LOADER_LOADED__ = true; window.__TAP_KIT_LOADER_LOADING__ = undefined; resolve(); return; } const elapsed = Date.now() - startTime; // Check if exceeded timeout if (elapsed > timeoutMs) { window.__TAP_KIT_LOADER_LOADING__ = undefined; reject(new Error(`TapKit loader timeout: SDK not available after ${timeoutMs}ms`)); return; } // Use requestIdleCallback for better performance // Falls back to setTimeout if not available if (typeof requestIdleCallback !== "undefined") { requestIdleCallback(checkSDK, { timeout: IDLE_CALLBACK_TIMEOUT_MS }); } else { setTimeout(checkSDK, IDLE_CALLBACK_TIMEOUT_MS); } }; return checkSDK; } /** * Loads the CDN loader script * The loader will then fetch versions.json and load the appropriate SDK version * * If __TAP_KIT_CORE_URL__ is set, bypasses loader and loads IIFE directly * * @param timeoutMs - Maximum time to wait for SDK to load (default: 4000ms) * @returns Promise that resolves when SDK is loaded * @throws {Error} If loader fails to load or times out */ export function loadCDNLoader(timeoutMs: number = DEFAULT_TIMEOUT_MS): Promise { // If already loaded, return immediately if (window.__TAP_KIT_LOADER_LOADED__ && window.TapKit) { return Promise.resolve(); } // If currently loading, return the existing promise if (window.__TAP_KIT_LOADER_LOADING__) { return window.__TAP_KIT_LOADER_LOADING__; } // Create loading promise const loadingPromise = new Promise((resolve, reject) => { if (typeof document === "undefined") { reject(new Error("TapKit requires browser environment (document is undefined)")); return; } // Local core mode: Load IIFE directly if (isLocalCoreMode()) { const coreURL = getLocalCoreURL(); console.log("[TapSDK] Loading local core:", coreURL); const script = document.createElement("script"); script.src = coreURL; script.async = true; script.onload = () => { // IIFE directly sets window.TapKit // Set the loaded flag manually since we bypass loader.js if (window.TapKit) { window.TapKitLoaded = true; window.__TAP_KIT_LOADER_LOADED__ = true; window.__TAP_KIT_LOADER_LOADING__ = undefined; console.log("[TapSDK] Local core loaded successfully"); resolve(); } else { window.__TAP_KIT_LOADER_LOADING__ = undefined; reject(new Error("TapKit not available after loading local core")); } }; script.onerror = () => { window.__TAP_KIT_LOADER_LOADING__ = undefined; reject(new Error(`Failed to load local TapKit core: ${coreURL}`)); }; document.head.appendChild(script); return; } // CDN mode: Load loader.js const loaderURL = getLoaderURL(); const script = document.createElement("script"); script.src = loaderURL; script.async = true; script.onload = () => { // The loader script will load the actual SDK // We need to wait a bit for the loader to fetch and load the SDK const checkSDK = createSDKChecker(resolve, reject, timeoutMs); checkSDK(); }; script.onerror = () => { window.__TAP_KIT_LOADER_LOADING__ = undefined; reject(new Error(`Failed to load TapKit CDN loader: ${loaderURL}`)); }; // Check if script already exists const existingScript = document.querySelector(`script[src="${loaderURL}"]`); if (existingScript) { // Script already added but not yet loaded existingScript.addEventListener("load", () => { const checkSDK = createSDKChecker(resolve, reject, timeoutMs); checkSDK(); }); existingScript.addEventListener("error", () => reject(new Error(`Failed to load TapKit CDN loader: ${loaderURL}`))); } else { document.head.appendChild(script); } }); window.__TAP_KIT_LOADER_LOADING__ = loadingPromise; return loadingPromise; }