Skip to main content

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

tip

Use promise wrappers for complex async operations, but keep simple callbacks for straightforward SDK calls to maintain clarity.