up
All checks were successful
Deploy to Production / deploy (push) Successful in 8s

This commit is contained in:
lubukhu
2026-01-24 13:35:11 +07:00
parent 6c3e93636e
commit 65fd0158a3
145 changed files with 10262 additions and 0 deletions

View File

@@ -0,0 +1,298 @@
"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