Skip to main content

Storage Management

The Moitribe SDK provides a cross-platform storage utility that automatically adapts to the available storage mechanism in your environment.

Storage Architecture

The SDK uses a tiered storage approach with automatic fallback:

  1. localStorage (Browser) - Primary choice for web applications
  2. Cookies (Browser) - Fallback when localStorage is unavailable
  3. In-Memory (Node.js/SSR) - Fallback for server-side environments

Basic Usage

Import and Initialize

import { Storage } from '@veniso/moitribe-js';

// Storage is automatically initialized on import
// No manual initialization required

Basic Operations

// Store data
Storage.setItem('player_name', 'Alex');
Storage.setItem('high_score', '1500');
Storage.setItem('game_settings', JSON.stringify({
sound: true,
music: false,
difficulty: 'medium'
}));

// Retrieve data
const playerName = Storage.getItem('player_name'); // 'Alex'
const highScore = Storage.getItem('high_score'); // '1500'
const settings = Storage.getItem('game_settings'); // '{"sound":true,"music":false,"difficulty":"medium"}'

// Parse JSON data
const gameSettings = JSON.parse(settings || '{}');

// Remove data
Storage.removeItem('player_name');
Storage.removeItem('high_score');

Advanced Usage

Type-Safe Storage Wrapper

Create a type-safe wrapper for your game data:

interface GameData {
playerName: string;
highScore: number;
lastPlayed: string;
settings: {
sound: boolean;
music: boolean;
difficulty: 'easy' | 'medium' | 'hard';
};
achievements: string[];
}

class GameStorage {
private static readonly KEYS = {
PLAYER_NAME: 'player_name',
HIGH_SCORE: 'high_score',
LAST_PLAYED: 'last_played',
SETTINGS: 'game_settings',
ACHIEVEMENTS: 'achievements'
};

static savePlayerName(name: string): void {
Storage.setItem(this.KEYS.PLAYER_NAME, name);
}

static getPlayerName(): string | null {
return Storage.getItem(this.KEYS.PLAYER_NAME);
}

static saveHighScore(score: number): void {
Storage.setItem(this.KEYS.HIGH_SCORE, score.toString());
}

static getHighScore(): number {
const score = Storage.getItem(this.KEYS.HIGH_SCORE);
return score ? parseInt(score, 10) : 0;
}

static saveSettings(settings: GameData['settings']): void {
Storage.setItem(this.KEYS.SETTINGS, JSON.stringify(settings));
}

static getSettings(): GameData['settings'] {
const settings = Storage.getItem(this.KEYS.SETTINGS);
return settings ? JSON.parse(settings) : {
sound: true,
music: false,
difficulty: 'medium'
};
}

static saveAchievements(achievements: string[]): void {
Storage.setItem(this.KEYS.ACHIEVEMENTS, JSON.stringify(achievements));
}

static getAchievements(): string[] {
const achievements = Storage.getItem(this.KEYS.ACHIEVEMENTS);
return achievements ? JSON.parse(achievements) : [];
}

static getAllGameData(): GameData {
return {
playerName: this.getPlayerName() || 'Guest',
highScore: this.getHighScore(),
lastPlayed: Storage.getItem(this.KEYS.LAST_PLAYED) || new Date().toISOString(),
settings: this.getSettings(),
achievements: this.getAchievements()
};
}

static clearAllGameData(): void {
Object.values(this.KEYS).forEach(key => {
Storage.removeItem(key);
});
}
}

// Usage
GameStorage.savePlayerName('Alex');
GameStorage.saveHighScore(1500);
GameStorage.saveSettings({
sound: true,
music: false,
difficulty: 'hard'
});

const gameData = GameStorage.getAllGameData();
console.log('Player:', gameData.playerName);
console.log('High Score:', gameData.highScore);

Storage with Expiration

Implement time-based data expiration:

interface ExpiringData {
value: any;
expires: number;
}

class ExpiringStorage {
static setWithExpiration(key: string, value: any, ttlMinutes: number): void {
const data: ExpiringData = {
value: value,
expires: Date.now() + (ttlMinutes * 60 * 1000)
};
Storage.setItem(key, JSON.stringify(data));
}

static getWithExpiration(key: string): any | null {
const data = Storage.getItem(key);
if (!data) return null;

try {
const parsed: ExpiringData = JSON.parse(data);

if (Date.now() > parsed.expires) {
// Data expired, remove it
Storage.removeItem(key);
return null;
}

return parsed.value;
} catch {
// Invalid data format, remove it
Storage.removeItem(key);
return null;
}
}

static setCachedProfile(profile: any, ttlMinutes: number = 30): void {
this.setWithExpiration('cached_profile', profile, ttlMinutes);
}

static getCachedProfile(): any | null {
return this.getWithExpiration('cached_profile');
}
}

// Usage
// Cache profile for 30 minutes
ExpiringStorage.setCachedProfile({
id: 'player_123',
name: 'Alex',
level: 42
}, 30);

// Retrieve cached profile
const cachedProfile = ExpiringStorage.getCachedProfile();
if (cachedProfile) {
console.log('Using cached profile:', cachedProfile.name);
} else {
console.log('Profile cache expired or not found');
}

Custom Storage Adapters

Create Custom Adapter

import { IStorage } from '@veniso/moitribe-js';

// Custom adapter for IndexedDB
class IndexedDBAdapter implements IStorage {
private dbName = 'MoitribeGameDB';
private storeName = 'gameStorage';
private db: IDBDatabase | null = null;

async init(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);

request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};

request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
});
}

getItem(key: string): string | null {
if (!this.db) return null;

return new Promise((resolve) => {
const transaction = this.db!.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get(key);

request.onsuccess = () => {
resolve(request.result || null);
};

request.onerror = () => {
resolve(null);
};
}) as any;
}

setItem(key: string, value: string): void {
if (!this.db) return;

const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
store.put(value, key);
}

removeItem(key: string): void {
if (!this.db) return;

const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
store.delete(key);
}
}

// Use custom adapter
const indexedDBAdapter = new IndexedDBAdapter();
indexedDBAdapter.init().then(() => {
Storage.setAdapter(indexedDBAdapter);
console.log('Using IndexedDB storage');
});

Server-Side Storage Adapter

// Custom adapter for file-based storage (Node.js)
import fs from 'fs';
import path from 'path';

class FileStorageAdapter implements IStorage {
private filePath: string;

constructor(filePath: string = './game-storage.json') {
this.filePath = path.resolve(filePath);
}

private loadData(): Record<string, string> {
try {
if (fs.existsSync(this.filePath)) {
const data = fs.readFileSync(this.filePath, 'utf8');
return JSON.parse(data);
}
} catch (error) {
console.error('Error loading storage file:', error);
}
return {};
}

private saveData(data: Record<string, string>): void {
try {
fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
} catch (error) {
console.error('Error saving storage file:', error);
}
}

getItem(key: string): string | null {
const data = this.loadData();
return data[key] || null;
}

setItem(key: string, value: string): void {
const data = this.loadData();
data[key] = value;
this.saveData(data);
}

removeItem(key: string): void {
const data = this.loadData();
delete data[key];
this.saveData(data);
}
}

// Use file storage in Node.js
if (typeof window === 'undefined') {
const fileAdapter = new FileStorageAdapter('./game-data.json');
Storage.setAdapter(fileAdapter);
}

Storage Monitoring

Storage Usage Analytics

class StorageMonitor {
static getStorageInfo(): {
adapter: string;
itemCount: number;
estimatedSize: number;
availableSpace?: number;
} {
const info = {
adapter: 'unknown',
itemCount: 0,
estimatedSize: 0,
availableSpace: undefined as number | undefined
};

// Determine adapter type
if (typeof localStorage !== 'undefined') {
info.adapter = 'localStorage';
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
const value = localStorage.getItem(key) || '';
info.estimatedSize += key.length + value.length;
info.itemCount++;
}
}

// Estimate available space (rough approximation)
if ('storage' in navigator && 'estimate' in navigator.storage) {
navigator.storage.estimate().then(estimate => {
info.availableSpace = (estimate.quota || 0) - (estimate.usage || 0);
});
}
} else {
info.adapter = 'in-memory';
// In-memory storage size estimation would require access to internal state
}

return info;
}

static logStorageUsage(): void {
const info = this.getStorageInfo();
console.log('Storage Information:');
console.log(`Adapter: ${info.adapter}`);
console.log(`Items: ${info.itemCount}`);
console.log(`Estimated size: ${(info.estimatedSize / 1024).toFixed(2)} KB`);
if (info.availableSpace !== undefined) {
console.log(`Available space: ${(info.availableSpace / 1024 / 1024).toFixed(2)} MB`);
}
}

static cleanupExpiredItems(): void {
// Clean up any expired items if using expiring storage
const keys = ['cached_profile', 'cached_leaderboard', 'session_token'];
keys.forEach(key => {
const data = Storage.getItem(key);
if (data) {
try {
const parsed = JSON.parse(data);
if (parsed.expires && Date.now() > parsed.expires) {
Storage.removeItem(key);
console.log(`Cleaned up expired item: ${key}`);
}
} catch {
// Invalid format, remove it
Storage.removeItem(key);
}
}
});
}
}

// Monitor storage usage
StorageMonitor.logStorageUsage();

// Clean up expired items periodically
setInterval(() => {
StorageMonitor.cleanupExpiredItems();
}, 5 * 60 * 1000); // Every 5 minutes

Environment-Specific Usage

Browser Environment

// Browser - automatically uses localStorage
if (typeof window !== 'undefined') {
// Check if localStorage is available
const storageAvailable = (() => {
try {
const test = '__storage_test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch {
return false;
}
})();

if (storageAvailable) {
console.log('Using localStorage');
} else {
console.log('localStorage not available, using cookies');
}
}

Node.js Environment

// Node.js - automatically uses in-memory storage
if (typeof window === 'undefined') {
console.log('Running in Node.js, using in-memory storage');

// You can set a custom adapter for persistence
const fileAdapter = new FileStorageAdapter('./game-storage.json');
Storage.setAdapter(fileAdapter);
}

Best Practices

1. Always Check for Null

// ✓ Good
const playerName = Storage.getItem('player_name');
if (playerName) {
console.log('Player:', playerName);
} else {
console.log('No player name found');
}

// ✗ Bad - could cause errors
const playerName = Storage.getItem('player_name')!;
console.log(playerName.length); // Error if playerName is null

2. Use JSON for Complex Data

// ✓ Good
const settings = { sound: true, music: false };
Storage.setItem('settings', JSON.stringify(settings));

const stored = Storage.getItem('settings');
const parsedSettings = stored ? JSON.parse(stored) : {};

// ✗ Bad - storing objects directly
Storage.setItem('settings', settings as any); // May not work correctly

3. Handle Storage Errors Gracefully

// ✓ Good - with error handling
try {
Storage.setItem('important_data', JSON.stringify(data));
} catch (error) {
console.error('Failed to save data:', error);
// Fallback to in-memory storage or notify user
}

// ✓ Good - using the built-in error handling
// The SDK automatically handles storage errors and falls back

4. Use Meaningful Key Names

// ✓ Good
Storage.setItem('player_profile_v2', JSON.stringify(profile));
Storage.setItem('high_scores_classic', JSON.stringify(scores));

// ✗ Bad
Storage.setItem('data1', JSON.stringify(profile));
Storage.setItem('temp', JSON.stringify(scores));

Next Steps

warning

Be mindful of storage limits in browsers. localStorage typically has a 5-10MB limit, and users can clear it at any time.