Callback Patterns
The Moitribe SDK uses callback-based patterns for asynchronous operations. This guide covers how to work with callbacks effectively and convert them to modern async/await patterns.
Basic Callback Pattern
All SDK methods follow this callback pattern:
MoitribeSDK(gameId, methodName, parameters, callback);
Simple Callback Example
MoitribeSDK('my-game', 'getprofile', {}, (result) => {
if (result.success) {
console.log('Profile loaded:', result.data);
} else {
console.error('Error:', result.message);
}
});
TypeScript Callback with Types
import { CallbackFunction, SignedInProfile } from '@veniso/moitribe-js';
const profileCallback: CallbackFunction<{ success: boolean; data?: SignedInProfile }> = (result) => {
if (result.success && result.data) {
console.log('Player name:', result.data.name);
console.log('Player level:', result.data.level);
}
};
MoitribeSDK('my-game', 'getprofile', {}, profileCallback);
Callback Chaining
Sequential Operations
// Load profile, then submit score
MoitribeSDK('my-game', 'getprofile', {}, (profileResult) => {
if (profileResult.success) {
console.log('Profile loaded for:', profileResult.data.name);
// Now submit score
MoitribeSDK('my-game', 'submitscore', {
leaderboardid: 'high-scores',
score: 1000
}, (scoreResult) => {
if (scoreResult.success) {
console.log('Score submitted successfully');
} else {
console.error('Score submission failed:', scoreResult.message);
}
});
} else {
console.error('Failed to load profile');
}
});
Parallel Operations
let completedOperations = 0;
const totalOperations = 2;
const checkCompletion = () => {
completedOperations++;
if (completedOperations === totalOperations) {
console.log('All operations completed');
}
};
// Load profile
MoitribeSDK('my-game', 'getprofile', {}, (result) => {
console.log('Profile loaded:', result.success);
checkCompletion();
});
// Load leaderboard
MoitribeSDK('my-game', 'loadleaderboardmetadata', {
leaderboardid: 'high-scores'
}, (result) => {
console.log('Leaderboard loaded:', result.success);
checkCompletion();
});
Promise Wrapper
Basic Promise Wrapper
Create a reusable promise wrapper for SDK methods:
class SDKPromise {
static wrap<T = any>(
gameId: string,
method: string,
params: any = {}
): Promise<T> {
return new Promise((resolve, reject) => {
MoitribeSDK(gameId, method, params, (result) => {
if (result.success) {
resolve(result);
} else {
reject(new Error(result.message || 'SDK operation failed'));
}
});
});
}
}
// Usage
async function loadProfile() {
try {
const result = await SDKPromise.wrap('my-game', 'getprofile');
console.log('Profile:', result.data);
} catch (error) {
console.error('Failed to load profile:', error.message);
}
}
Advanced Promise Wrapper with Error Handling
import { StatusCodes } from '@veniso/moitribe-js';
interface SDKError extends Error {
code?: number;
originalResult?: any;
}
class SDKPromise {
static wrap<T = any>(
gameId: string,
method: string,
params: any = {}
): Promise<T> {
return new Promise((resolve, reject) => {
MoitribeSDK(gameId, method, params, (result) => {
if (result.success) {
resolve(result);
} else {
const error: SDKError = new Error(result.message || 'SDK operation failed');
error.code = result.code;
error.originalResult = result;
reject(error);
}
});
});
}
static wrapWithRetry<T = any>(
gameId: string,
method: string,
params: any = {},
maxRetries: number = 3,
delay: number = 1000
): Promise<T> {
return new Promise((resolve, reject) => {
let attempts = 0;
const attempt = () => {
attempts++;
this.wrap<T>(gameId, method, params)
.then(resolve)
.catch((error: SDKError) => {
// Don't retry on authentication errors
if (error.code === StatusCodes.STATUS_SIGNED_IN_PLAYER_REQUIRED ||
attempts >= maxRetries) {
reject(error);
} else {
console.log(`Attempt ${attempts} failed, retrying...`);
setTimeout(attempt, delay * attempts);
}
});
};
attempt();
});
}
}
// Usage with retry
async function submitScoreWithRetry(score: number) {
try {
const result = await SDKPromise.wrapWithRetry('my-game', 'submitscore', {
leaderboardid: 'high-scores',
score: score
});
console.log('Score submitted:', result.success);
} catch (error: any) {
console.error('Failed to submit score after retries:', error.message);
if (error.code === StatusCodes.STATUS_SIGNED_IN_PLAYER_REQUIRED) {
console.log('Authentication required');
}
}
}
Async/Await Patterns
Sequential Async Operations
async function gameSession() {
try {
// 1. Check authentication
const authResult = await SDKPromise.wrap('my-game', 'isAuthenticated');
if (!authResult.authenticated) {
console.log('Player not authenticated');
return;
}
// 2. Load profile
const profileResult = await SDKPromise.wrap('my-game', 'getprofile');
console.log('Welcome back,', profileResult.data.name);
// 3. Load leaderboard metadata
const leaderboardResult = await SDKPromise.wrap('my-game', 'loadleaderboardmetadata', {
leaderboardid: 'high-scores'
});
// 4. Submit score
const scoreResult = await SDKPromise.wrap('my-game', 'submitscore', {
leaderboardid: 'high-scores',
score: 1500
});
console.log('Game session completed successfully');
} catch (error: any) {
console.error('Game session failed:', error.message);
}
}
Parallel Async Operations
async function loadGameData() {
try {
// Load multiple data sources in parallel
const [profileResult, leaderboardResult, tournamentResult] = await Promise.all([
SDKPromise.wrap('my-game', 'getprofile'),
SDKPromise.wrap('my-game', 'loadleaderboardmetadata', { leaderboardid: 'high-scores' }),
SDKPromise.wrap('my-game', 'gettournamentmetadata')
]);
console.log('All game data loaded');
console.log('Profile:', profileResult.data.name);
console.log('Leaderboard:', leaderboardResult.data.name);
console.log('Tournaments:', tournamentResult.data.length);
} catch (error: any) {
console.error('Failed to load game data:', error.message);
}
}
Race Conditions
async function waitForFirstAvailable() {
try {
// Wait for first available operation to complete
const result = await Promise.race([
SDKPromise.wrap('my-game', 'getprofile'),
SDKPromise.wrap('my-game', 'loadleaderboardmetadata', { leaderboardid: 'high-scores' })
]);
console.log('First operation completed:', result);
} catch (error: any) {
console.error('All operations failed:', error.message);
}
}
Event-Driven Patterns
Event Emitter for SDK Events
class SDKEventManager {
private listeners: Map<string, Function[]> = new Map();
on(event: string, callback: Function): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event)!.push(callback);
}
emit(event: string, data?: any): void {
const callbacks = this.listeners.get(event) || [];
callbacks.forEach(callback => callback(data));
}
off(event: string, callback: Function): void {
const callbacks = this.listeners.get(event) || [];
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
const sdkEvents = new SDKEventManager();
// Wrap SDK calls to emit events
function emitSDKCall<T>(gameId: string, method: string, params: any = {}): Promise<T> {
sdkEvents.emit('sdkCallStart', { method, params });
return SDKPromise.wrap<T>(gameId, method, params)
.then(result => {
sdkEvents.emit('sdkCallSuccess', { method, result });
return result;
})
.catch(error => {
sdkEvents.emit('sdkCallError', { method, error });
throw error;
});
}
// Usage with events
sdkEvents.on('sdkCallStart', ({ method }) => {
console.log(`Starting ${method}...`);
});
sdkEvents.on('sdkCallSuccess', ({ method, result }) => {
console.log(`${method} completed successfully`);
});
sdkEvents.on('sdkCallError', ({ method, error }) => {
console.error(`${method} failed:`, error.message);
});
Real-Time Multiplayer Callbacks
Room Event Handlers
const roomCallbacks = {
onPlayerJoined: (participant: Participant) => {
console.log('Player joined:', participant.playerName);
updatePlayerList();
},
onPlayerLeft: (participant: Participant) => {
console.log('Player left:', participant.playerName);
updatePlayerList();
},
onMessageReceived: (message: RealTimeMessage) => {
console.log('Message received from:', message.senderId);
handleGameMessage(message);
},
onRoomStateChanged: (room: Room) => {
console.log('Room state changed:', room.status);
updateRoomUI(room);
}
};
// Set up room with callbacks
MoitribeSDK('my-game', 'createstandardroom', {
maxPlayers: 4,
variant: 1
}, (result) => {
if (result.success) {
console.log('Room created:', result.roomId);
// Room callbacks would be set up through the room service
}
});
Message Handling Patterns
class MessageHandler {
private handlers: Map<string, (data: any) => void> = new Map();
register(messageType: string, handler: (data: any) => void): void {
this.handlers.set(messageType, handler);
}
handle(message: RealTimeMessage): void {
try {
const data = JSON.parse(new TextDecoder().decode(message.data));
const handler = this.handlers.get(data.type);
if (handler) {
handler(data.payload);
} else {
console.warn('No handler for message type:', data.type);
}
} catch (error) {
console.error('Failed to parse message:', error);
}
}
}
const messageHandler = new MessageHandler();
// Register message handlers
messageHandler.register('player_move', (data) => {
console.log('Player moved:', data.playerId, data.position);
updatePlayerPosition(data.playerId, data.position);
});
messageHandler.register('game_state', (data) => {
console.log('Game state updated:', data);
updateGameState(data);
});
// Use in room message callback
const onMessageReceived = (message: RealTimeMessage) => {
messageHandler.handle(message);
};
Best Practices
1. Always Handle Errors
// ✓ Good
MoitribeSDK('my-game', 'getprofile', {}, (result) => {
if (result.success) {
// Handle success
} else {
// Handle error
}
});
// ✗ Bad
MoitribeSDK('my-game', 'getprofile', {}, (result) => {
// Only handles success case
});
2. Use Type Safety
// ✓ Good with TypeScript
const callback: CallbackFunction<{ success: boolean; data?: SignedInProfile }> = (result) => {
// TypeScript knows the structure
};
// ✗ Less safe
const callback = (result: any) => {
// No type checking
};
3. Avoid Callback Hell
// ✗ Bad - Callback hell
MoitribeSDK('my-game', 'getprofile', {}, (profileResult) => {
if (profileResult.success) {
MoitribeSDK('my-game', 'submitscore', { score: 1000 }, (scoreResult) => {
if (scoreResult.success) {
MoitribeSDK('my-game', 'loadleaderboardtopscores', {}, (leaderboardResult) => {
// Deep nesting...
});
}
});
}
});
// ✓ Good - Promise chain
async function gameFlow() {
const profileResult = await SDKPromise.wrap('my-game', 'getprofile');
const scoreResult = await SDKPromise.wrap('my-game', 'submitscore', { score: 1000 });
const leaderboardResult = await SDKPromise.wrap('my-game', 'loadleaderboardtopscores');
}
Next Steps
- Error Handling - Comprehensive error management
- Connection Callbacks - Monitor connection state
- Type Definitions - Complete type reference
tip
Use promise wrappers for complex async operations, but keep simple callbacks for straightforward SDK calls to maintain clarity.