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,108 @@
/**
* 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 { SdkInitPayload, SdkPushDataPayload, SdkReadyPayload, SdkDataReadyPayload, SdkAnswerResultPayload, SdkSyncStatusPayload, SdkSyncErrorPayload, SdkFinalResultPayload, SdkErrorPayload } from '../sdk-iframe/types';
export interface GameBridgeConfig {
sdkIframeUrl: string;
debug?: boolean;
timeout?: number;
}
export interface GameBridgeEvents {
ready: SdkReadyPayload;
dataReady: SdkDataReadyPayload;
answerResult: SdkAnswerResultPayload;
syncStatus: SdkSyncStatusPayload;
syncError: SdkSyncErrorPayload;
finalResult: SdkFinalResultPayload;
error: SdkErrorPayload;
}
type EventHandler<T> = (data: T) => void;
export declare class GameBridge {
private config;
private sdkIframe;
private sdkOrigin;
private isReady;
private handlers;
private pendingRequests;
private requestCounter;
constructor(config: GameBridgeConfig);
/**
* Create SDK Iframe and initialize
*/
init(payload: SdkInitPayload): Promise<SdkReadyPayload>;
/**
* Push data (preview mode)
*/
pushData(payload: SdkPushDataPayload): Promise<SdkDataReadyPayload>;
/**
* Check answer - returns local result immediately
* Also triggers server sync in background
*/
checkAnswer(questionId: string, choice: any, timeSpent?: number): Promise<SdkAnswerResultPayload>;
/**
* Get final result
*/
getFinalResult(): Promise<SdkFinalResultPayload>;
/**
* Retry sync for a question
*/
retrySync(questionId: string): Promise<SdkSyncStatusPayload>;
/**
* Subscribe to events
*/
on<K extends keyof GameBridgeEvents>(event: K, handler: EventHandler<GameBridgeEvents[K]>): () => void;
/**
* Unsubscribe from events
*/
off<K extends keyof GameBridgeEvents>(event: K, handler: EventHandler<GameBridgeEvents[K]>): void;
/**
* Check if SDK is ready
*/
isSdkReady(): boolean;
/**
* Destroy bridge and cleanup
*/
destroy(): void;
private setupMessageListener;
private handleMessage;
private emit;
private sendRequest;
private log;
}
/**
* Get or create GameBridge instance
*/
export declare function getGameBridge(config?: GameBridgeConfig): GameBridge;
/**
* Destroy GameBridge instance
*/
export declare function destroyGameBridge(): void;
export {};
//# sourceMappingURL=GameBridge.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"GameBridge.d.ts","sourceRoot":"","sources":["../../src/game-bridge/GameBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAGH,cAAc,EACd,kBAAkB,EAElB,eAAe,EACf,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EAGlB,MAAM,qBAAqB,CAAC;AAM7B,MAAM,WAAW,gBAAgB;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,eAAe,CAAC;IACvB,SAAS,EAAE,mBAAmB,CAAC;IAC/B,YAAY,EAAE,sBAAsB,CAAC;IACrC,UAAU,EAAE,oBAAoB,CAAC;IACjC,SAAS,EAAE,mBAAmB,CAAC;IAC/B,WAAW,EAAE,qBAAqB,CAAC;IACnC,KAAK,EAAE,eAAe,CAAC;CAC1B;AAED,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAMzC,qBAAa,UAAU;IACnB,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,SAAS,CAAkC;IACnD,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,OAAO,CAAS;IAGxB,OAAO,CAAC,QAAQ,CAAkE;IAGlF,OAAO,CAAC,eAAe,CAIR;IAEf,OAAO,CAAC,cAAc,CAAK;gBAEf,MAAM,EAAE,gBAAgB;IAsBpC;;OAEG;IACG,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAmB7D;;OAEG;IACG,QAAQ,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAQzE;;;OAGG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC;IASvG;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,qBAAqB,CAAC;IAItD;;OAEG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAUlE;;OAEG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAQtG;;OAEG;IACH,GAAG,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAQjG;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB;;OAEG;IACH,OAAO,IAAI,IAAI;IAwBf,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,aAAa;IA4DrB,OAAO,CAAC,IAAI;IAcZ,OAAO,CAAC,WAAW;IA0BnB,OAAO,CAAC,GAAG;CAiBd;AAQD;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,UAAU,CAQnE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC"}

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
/**
* Game Bridge exports
* Dành cho game developers tích hợp vào game
*/
export { GameBridge, getGameBridge, destroyGameBridge, type GameBridgeConfig, type GameBridgeEvents, } from './GameBridge';
export { SDK_MESSAGE_TYPES, type SdkMessageType, type SdkInitPayload, type SdkPushDataPayload, type SdkCheckAnswerPayload, type SdkReadyPayload, type SdkDataReadyPayload, type SdkAnswerResultPayload, type SdkSyncStatusPayload, type SdkSyncErrorPayload, type SdkFinalResultPayload, type SdkErrorPayload, } from '../sdk-iframe/types';
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/game-bridge/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACH,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,GACxB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACH,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,GACvB,MAAM,qBAAqB,CAAC"}

View File

@@ -0,0 +1,15 @@
"use strict";
/**
* Game Bridge exports
* Dành cho game developers tích hợp vào game
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SDK_MESSAGE_TYPES = exports.destroyGameBridge = exports.getGameBridge = exports.GameBridge = void 0;
var GameBridge_1 = require("./GameBridge");
Object.defineProperty(exports, "GameBridge", { enumerable: true, get: function () { return GameBridge_1.GameBridge; } });
Object.defineProperty(exports, "getGameBridge", { enumerable: true, get: function () { return GameBridge_1.getGameBridge; } });
Object.defineProperty(exports, "destroyGameBridge", { enumerable: true, get: function () { return GameBridge_1.destroyGameBridge; } });
// Re-export types từ sdk-iframe
var types_1 = require("../sdk-iframe/types");
Object.defineProperty(exports, "SDK_MESSAGE_TYPES", { enumerable: true, get: function () { return types_1.SDK_MESSAGE_TYPES; } });
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/game-bridge/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,2CAMsB;AALlB,wGAAA,UAAU,OAAA;AACV,2GAAA,aAAa,OAAA;AACb,+GAAA,iBAAiB,OAAA;AAKrB,gCAAgC;AAChC,6CAa6B;AAZzB,0GAAA,iBAAiB,OAAA"}