"use strict"; /** * Game Bridge * Wrapper đơn giản cho game developers để giao tiếp với SDK Iframe * * Usage: * ```typescript * import { GameBridge } from 'game-iframe-sdk/game-bridge'; * * const bridge = new GameBridge({ * sdkIframeUrl: 'https://sdk.sena.tech/sdk-iframe.html', * debug: true, * }); * * // Init * await bridge.init({ * mode: 'preview', * game_code: 'G001', * }); * * // Listen for data * bridge.on('dataReady', (data) => { * renderGame(data.items); * }); * * // Check answer * bridge.checkAnswer('q1', userChoice).then(result => { * showFeedback(result.correct); * }); * ``` */ Object.defineProperty(exports, "__esModule", { value: true }); exports.GameBridge = void 0; exports.getGameBridge = getGameBridge; exports.destroyGameBridge = destroyGameBridge; const types_1 = require("../sdk-iframe/types"); // ============================================================================= // GAME BRIDGE // ============================================================================= class GameBridge { constructor(config) { this.sdkIframe = null; this.sdkOrigin = ''; this.isReady = false; // Event handlers this.handlers = new Map(); // Pending requests (for promise-based API) this.pendingRequests = new Map(); this.requestCounter = 0; this.config = { sdkIframeUrl: config.sdkIframeUrl, debug: config.debug ?? false, timeout: config.timeout ?? 10000, }; // Extract origin from SDK URL try { const url = new URL(this.config.sdkIframeUrl); this.sdkOrigin = url.origin; } catch { this.sdkOrigin = '*'; } this.setupMessageListener(); } // ========================================================================== // PUBLIC API - Init // ========================================================================== /** * Create SDK Iframe and initialize */ async init(payload) { // Create hidden iframe this.sdkIframe = document.createElement('iframe'); this.sdkIframe.src = this.config.sdkIframeUrl; this.sdkIframe.style.cssText = 'position:absolute;width:0;height:0;border:0;visibility:hidden;'; this.sdkIframe.setAttribute('aria-hidden', 'true'); document.body.appendChild(this.sdkIframe); this.log('info', 'SDK Iframe created'); // Wait for iframe to load await new Promise((resolve) => { this.sdkIframe.onload = () => resolve(); }); // Send init return this.sendRequest(types_1.SDK_MESSAGE_TYPES.SDK_INIT, payload); } /** * Push data (preview mode) */ async pushData(payload) { return this.sendRequest(types_1.SDK_MESSAGE_TYPES.SDK_PUSH_DATA, payload); } // ========================================================================== // PUBLIC API - Game Actions // ========================================================================== /** * Check answer - returns local result immediately * Also triggers server sync in background */ async checkAnswer(questionId, choice, timeSpent) { const payload = { question_id: questionId, choice, time_spent: timeSpent, }; return this.sendRequest(types_1.SDK_MESSAGE_TYPES.SDK_CHECK_ANSWER, payload); } /** * Get final result */ async getFinalResult() { return this.sendRequest(types_1.SDK_MESSAGE_TYPES.SDK_GET_RESULT, {}); } /** * Retry sync for a question */ async retrySync(questionId) { return this.sendRequest(types_1.SDK_MESSAGE_TYPES.SDK_RETRY_SYNC, { question_id: questionId, }); } // ========================================================================== // PUBLIC API - Events // ========================================================================== /** * Subscribe to events */ on(event, handler) { if (!this.handlers.has(event)) { this.handlers.set(event, new Set()); } this.handlers.get(event).add(handler); return () => this.off(event, handler); } /** * Unsubscribe from events */ off(event, handler) { this.handlers.get(event)?.delete(handler); } // ========================================================================== // PUBLIC API - State // ========================================================================== /** * Check if SDK is ready */ isSdkReady() { return this.isReady; } /** * Destroy bridge and cleanup */ destroy() { // Clear pending requests this.pendingRequests.forEach((pending) => { clearTimeout(pending.timeout); pending.reject(new Error('Bridge destroyed')); }); this.pendingRequests.clear(); // Remove iframe if (this.sdkIframe && this.sdkIframe.parentNode) { this.sdkIframe.parentNode.removeChild(this.sdkIframe); } this.sdkIframe = null; // Clear handlers this.handlers.clear(); this.log('info', 'Bridge destroyed'); } // ========================================================================== // INTERNAL - Message Handling // ========================================================================== setupMessageListener() { window.addEventListener('message', this.handleMessage.bind(this)); } handleMessage(event) { // Validate origin (if not *) if (this.sdkOrigin !== '*' && event.origin !== this.sdkOrigin) { return; } const data = event.data; if (!(0, types_1.isSdkMessage)(data)) { return; } this.log('debug', `Received: ${data.type}`, data.payload); // Handle pending request response if (data.request_id && this.pendingRequests.has(data.request_id)) { const pending = this.pendingRequests.get(data.request_id); clearTimeout(pending.timeout); this.pendingRequests.delete(data.request_id); if (data.type === types_1.SDK_MESSAGE_TYPES.SDK_ERROR) { pending.reject(data.payload); } else { pending.resolve(data.payload); } return; } // Handle events switch (data.type) { case types_1.SDK_MESSAGE_TYPES.SDK_READY: this.isReady = true; this.emit('ready', data.payload); break; case types_1.SDK_MESSAGE_TYPES.SDK_DATA_READY: this.emit('dataReady', data.payload); break; case types_1.SDK_MESSAGE_TYPES.SDK_ANSWER_RESULT: this.emit('answerResult', data.payload); break; case types_1.SDK_MESSAGE_TYPES.SDK_SYNC_STATUS: this.emit('syncStatus', data.payload); break; case types_1.SDK_MESSAGE_TYPES.SDK_SYNC_ERROR: this.emit('syncError', data.payload); break; case types_1.SDK_MESSAGE_TYPES.SDK_FINAL_RESULT: this.emit('finalResult', data.payload); break; case types_1.SDK_MESSAGE_TYPES.SDK_ERROR: this.emit('error', data.payload); break; } } emit(event, data) { this.handlers.get(event)?.forEach(handler => { try { handler(data); } catch (err) { this.log('error', `Error in ${event} handler`, err); } }); } // ========================================================================== // INTERNAL - Sending Messages // ========================================================================== sendRequest(type, payload) { return new Promise((resolve, reject) => { if (!this.sdkIframe?.contentWindow) { reject(new Error('SDK Iframe not ready')); return; } const requestId = `req_${++this.requestCounter}_${Date.now()}`; const message = (0, types_1.createSdkMessage)(type, payload, requestId); // Setup timeout const timeout = setTimeout(() => { this.pendingRequests.delete(requestId); reject(new Error(`Request timeout: ${type}`)); }, this.config.timeout); // Store pending request this.pendingRequests.set(requestId, { resolve, reject, timeout }); // Send message this.sdkIframe.contentWindow.postMessage(message, this.sdkOrigin); this.log('debug', `Sent: ${type}`, payload); }); } log(level, message, data) { if (!this.config.debug && level === 'debug') return; const prefix = '[GameBridge]'; switch (level) { case 'debug': case 'info': console.log(prefix, message, data ?? ''); break; case 'warn': console.warn(prefix, message, data ?? ''); break; case 'error': console.error(prefix, message, data ?? ''); break; } } } exports.GameBridge = GameBridge; // ============================================================================= // FACTORY // ============================================================================= let bridgeInstance = null; /** * Get or create GameBridge instance */ function getGameBridge(config) { if (!bridgeInstance && config) { bridgeInstance = new GameBridge(config); } if (!bridgeInstance) { throw new Error('GameBridge not initialized. Call with config first.'); } return bridgeInstance; } /** * Destroy GameBridge instance */ function destroyGameBridge() { bridgeInstance?.destroy(); bridgeInstance = null; } //# sourceMappingURL=GameBridge.js.map