298 lines
10 KiB
JavaScript
298 lines
10 KiB
JavaScript
"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
|