This commit is contained in:
292
G102-sequence/sdk/package/dist/esm/game-bridge/GameBridge.js
vendored
Normal file
292
G102-sequence/sdk/package/dist/esm/game-bridge/GameBridge.js
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* 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);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
import { SDK_MESSAGE_TYPES, createSdkMessage, isSdkMessage, } from '../sdk-iframe/types';
|
||||
// =============================================================================
|
||||
// GAME BRIDGE
|
||||
// =============================================================================
|
||||
export 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(SDK_MESSAGE_TYPES.SDK_INIT, payload);
|
||||
}
|
||||
/**
|
||||
* Push data (preview mode)
|
||||
*/
|
||||
async pushData(payload) {
|
||||
return this.sendRequest(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(SDK_MESSAGE_TYPES.SDK_CHECK_ANSWER, payload);
|
||||
}
|
||||
/**
|
||||
* Get final result
|
||||
*/
|
||||
async getFinalResult() {
|
||||
return this.sendRequest(SDK_MESSAGE_TYPES.SDK_GET_RESULT, {});
|
||||
}
|
||||
/**
|
||||
* Retry sync for a question
|
||||
*/
|
||||
async retrySync(questionId) {
|
||||
return this.sendRequest(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 (!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 === SDK_MESSAGE_TYPES.SDK_ERROR) {
|
||||
pending.reject(data.payload);
|
||||
}
|
||||
else {
|
||||
pending.resolve(data.payload);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Handle events
|
||||
switch (data.type) {
|
||||
case SDK_MESSAGE_TYPES.SDK_READY:
|
||||
this.isReady = true;
|
||||
this.emit('ready', data.payload);
|
||||
break;
|
||||
case SDK_MESSAGE_TYPES.SDK_DATA_READY:
|
||||
this.emit('dataReady', data.payload);
|
||||
break;
|
||||
case SDK_MESSAGE_TYPES.SDK_ANSWER_RESULT:
|
||||
this.emit('answerResult', data.payload);
|
||||
break;
|
||||
case SDK_MESSAGE_TYPES.SDK_SYNC_STATUS:
|
||||
this.emit('syncStatus', data.payload);
|
||||
break;
|
||||
case SDK_MESSAGE_TYPES.SDK_SYNC_ERROR:
|
||||
this.emit('syncError', data.payload);
|
||||
break;
|
||||
case SDK_MESSAGE_TYPES.SDK_FINAL_RESULT:
|
||||
this.emit('finalResult', data.payload);
|
||||
break;
|
||||
case 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 = 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// =============================================================================
|
||||
// FACTORY
|
||||
// =============================================================================
|
||||
let bridgeInstance = null;
|
||||
/**
|
||||
* Get or create GameBridge instance
|
||||
*/
|
||||
export 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
|
||||
*/
|
||||
export function destroyGameBridge() {
|
||||
bridgeInstance?.destroy();
|
||||
bridgeInstance = null;
|
||||
}
|
||||
//# sourceMappingURL=GameBridge.js.map
|
||||
Reference in New Issue
Block a user