"use strict";
/**
* SenaGame SDK Loader
*
* Ready-to-use interface for game developers.
* Handles SDK iframe creation, communication, and lifecycle.
*
* Usage:
* ```html
*
*
* ```
*
* @version 1.0.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SenaGameSDK = void 0;
// ========================================
// MESSAGE TYPES (matching SDK)
// ========================================
const MSG = {
// Outgoing (Game → SDK)
INIT: 'SDK_INIT',
PUSH_DATA: 'SDK_PUSH_DATA',
SUBMIT_ANSWER: 'SDK_CHECK_ANSWER',
COMPLETE_GAME: 'SDK_COMPLETE_GAME',
GET_STATUS: 'SDK_GET_STATUS',
// Incoming (SDK → Game)
READY: 'SDK_READY',
DATA_READY: 'SDK_DATA_READY',
ANSWER_RESULT: 'SDK_ANSWER_RESULT',
GAME_COMPLETE: 'SDK_GAME_COMPLETE',
SESSION_STARTED: 'SDK_SESSION_STARTED',
STATUS: 'SDK_STATUS',
ERROR: 'SDK_ERROR',
};
// ========================================
// DEFAULT CONFIG
// ========================================
const DEFAULT_CONFIG = {
iframePath: '../sdk-iframe/index.html',
mode: 'preview',
gameCode: '',
debug: false,
timeout: 10000,
iframeStyle: 'position:fixed;width:1px;height:1px;left:-9999px;border:none;',
};
// ========================================
// SENA GAME SDK CLASS
// ========================================
class SenaGameSDK {
constructor(config) {
this.iframe = null;
this.isReady = false;
this.isDataReady = false;
this.pendingMessages = [];
this.initResolver = null;
this.timeoutId = null;
this.config = { ...DEFAULT_CONFIG, ...config };
// --- CLEANUP OLD LISTENERS ---
// Tránh trường hợp init SDK nhiều lần bị trùng listener cũ
const oldSDK = window._sena_game_sdk_instance;
if (oldSDK && typeof oldSDK.destroy === 'function') {
oldSDK.destroy();
}
window._sena_game_sdk_instance = this;
// Create promise for ready state
this.initPromise = new Promise((resolve, reject) => {
this.initResolver = { resolve, reject };
});
// Bind methods
this._handleMessage = this._handleMessage.bind(this);
// Auto-initialize
this._init();
}
// ----------------------------------------
// PUBLIC API
// ----------------------------------------
/**
* Push game data to SDK
*/
pushData(data) {
// Safe extraction of the array
let itemsArray = [];
if (Array.isArray(data)) {
itemsArray = data;
}
else if (data && Array.isArray(data.items)) {
itemsArray = data.items;
}
else if (data && Array.isArray(data.data)) {
itemsArray = data.data;
}
else if (data && data.items && typeof data.items === 'object') {
itemsArray = [data.items];
}
else if (data && data.data && typeof data.data === 'object') {
itemsArray = [data.data];
}
// Transform to SDK iframe internal format
const payload = {
data: itemsArray,
completed_question_ids: (data && data.completed_question_ids) || []
};
this._send(MSG.PUSH_DATA, payload);
}
/**
* Submit an answer
*/
submitAnswer(answer) {
// Transform to SDK iframe format
const payload = {
question_id: answer.questionId,
choice: answer.selectedAnswer,
time_spent: answer.timeSpent ?? 0
};
this._send(MSG.SUBMIT_ANSWER, payload);
}
/**
* Complete the game
*/
completeGame() {
this._send(MSG.COMPLETE_GAME, {});
}
/**
* Get current status
*/
getStatus() {
this._send(MSG.GET_STATUS, {});
}
/**
* Wait for SDK to be ready
*/
async ready() {
if (this.isReady)
return this;
return this.initPromise;
}
/**
* Check if SDK is ready
*/
get sdkReady() {
return this.isReady;
}
/**
* Check if data is ready
*/
get dataReady() {
return this.isDataReady;
}
/**
* Destroy the SDK instance
*/
destroy() {
window.removeEventListener('message', this._handleMessage);
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
if (this.iframe && this.iframe.parentNode) {
this.iframe.parentNode.removeChild(this.iframe);
}
window.removeEventListener('message', this._handleMessage);
this.isReady = false;
this._log('SDK destroyed');
}
// ----------------------------------------
// PRIVATE METHODS
// ----------------------------------------
_init() {
this._log('Initializing SenaGameSDK...');
// Setup message listener
window.addEventListener('message', this._handleMessage);
// Create iframe
this._createIframe();
// Setup timeout
this.timeoutId = setTimeout(() => {
if (!this.isReady) {
const error = new Error('SDK initialization timeout');
this._error(error);
if (this.initResolver) {
this.initResolver.reject(error);
}
}
}, this.config.timeout);
}
_createIframe() {
this.iframe = document.createElement('iframe');
this.iframe.id = 'sena-game-sdk-iframe';
this.iframe.src = this.config.iframePath;
this.iframe.style.cssText = this.config.iframeStyle || '';
this.iframe.onload = () => {
this._log('Iframe loaded, sending INIT...');
setTimeout(() => {
this._send(MSG.INIT, {
mode: this.config.mode,
game_code: this.config.gameCode,
});
}, 100);
};
this.iframe.onerror = () => {
this._error(new Error('Failed to load SDK iframe'));
};
document.body.appendChild(this.iframe);
this._log(`Iframe created: ${this.config.iframePath}`);
}
_send(type, payload) {
if (!this.iframe || !this.iframe.contentWindow) {
this._log(`Queuing message: ${type}`, 'warn');
this.pendingMessages.push({ type, payload });
return;
}
const message = { type, payload, timestamp: Date.now() };
this._log(`→ ${type}`, 'send');
this.iframe.contentWindow.postMessage(message, '*');
}
_handleMessage(event) {
const data = event.data;
if (!data || !data.type)
return;
// Only process SDK messages
if (!data.type.startsWith('SDK_'))
return;
this._log(`← ${data.type}`, 'recv');
switch (data.type) {
case MSG.READY:
this._onSDKReady();
break;
case MSG.DATA_READY:
this._onDataReady(data.payload);
break;
case MSG.ANSWER_RESULT:
this._onAnswerResult(data.payload);
break;
case MSG.GAME_COMPLETE:
this._onGameComplete(data.payload);
break;
case MSG.SESSION_STARTED:
this._onSessionStart(data.payload);
break;
case MSG.ERROR:
this._error(new Error(data.payload?.message || 'SDK Error'));
break;
}
}
_onSDKReady() {
this.isReady = true;
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
// Flush pending messages
while (this.pendingMessages.length > 0) {
const msg = this.pendingMessages.shift();
if (msg) {
this._send(msg.type, msg.payload);
}
}
// Callback
if (this.config.onReady) {
this.config.onReady(this);
}
// Resolve promise
if (this.initResolver) {
this.initResolver.resolve(this);
}
this._log('✅ SDK Ready!');
}
_onDataReady(payload) {
this.isDataReady = true;
// Normalize payload to camelCase
const normalized = {
items: payload.items || [],
totalQuestions: payload.total_questions || payload.totalQuestions || 0,
completedCount: payload.completed_count || payload.completedCount || 0,
resumeData: payload.resume_data || payload.resumeData || []
};
if (this.config.onDataReady) {
this.config.onDataReady(normalized);
}
this._log(`✅ Data Ready: ${normalized.items.length} items`);
}
_onAnswerResult(payload) {
const normalized = {
questionId: payload.question_id || payload.questionId,
isCorrect: payload.correct !== undefined ? payload.correct : payload.isCorrect,
correctAnswer: payload.correct_answer || payload.correctAnswer || '',
score: payload.score || 0,
currentScore: payload.current_score || payload.currentScore || 0,
totalAnswered: payload.total_answered || payload.totalAnswered || 0
};
if (this.config.onAnswerResult) {
this.config.onAnswerResult(normalized);
}
}
_onGameComplete(payload) {
const normalized = {
success: payload.success !== undefined ? payload.success : true,
finalScore: payload.score !== undefined ? payload.score : (payload.finalScore || 0),
correctCount: payload.correct !== undefined ? payload.correct : (payload.correctCount || 0),
totalQuestions: payload.total !== undefined ? payload.total : (payload.totalQuestions || 0),
wrongCount: payload.wrong !== undefined ? payload.wrong : (payload.wrongCount || 0),
total: payload.total || 0
};
if (this.config.onGameComplete) {
this.config.onGameComplete(normalized);
}
}
_onSessionStart(payload) {
const normalized = {
assignmentId: payload.assignment_id || payload.assignmentId,
userId: payload.student_id || payload.userId,
gameId: payload.game_code || payload.gameId,
startedAt: payload.started_at || payload.startedAt || new Date().toISOString()
};
if (this.config.onSessionStart) {
this.config.onSessionStart(normalized);
}
}
_error(error) {
this._log(`❌ Error: ${error.message}`, 'error');
if (this.config.onError) {
this.config.onError(error);
}
}
_log(message, type = 'info') {
if (!this.config.debug)
return;
const prefix = '[SenaGameSDK]';
const styles = {
info: 'color: #888',
send: 'color: #ff0',
recv: 'color: #0f0',
warn: 'color: #fa0',
error: 'color: #f00',
};
console.log(`%c${prefix} ${message}`, styles[type] || styles.info);
}
}
exports.SenaGameSDK = SenaGameSDK;
/** SDK version */
SenaGameSDK.VERSION = '1.0.0';
// ========================================
// EXPORT FOR BROWSER (UMD)
// ========================================
if (typeof window !== 'undefined') {
window.SenaGameSDK = SenaGameSDK;
}
exports.default = SenaGameSDK;
//# sourceMappingURL=SenaGameSDK.js.map