Persistent Game Data
Save player progress, settings, and scores with cloud or local storage. The Hyve SDK provides flexible persistence that works across devices or locally for offline functionality.
Overview
The persistence system supports two storage backends:
- Cloud Storage: Syncs across devices, requires authentication
- Local Storage: Device-specific, works offline, no authentication needed
All data is automatically scoped to your game and the authenticated user (for cloud storage).
Quick Start
import { HyveClient } from '@hyve-sdk/js';
const client = new HyveClient({
storageMode: 'cloud' // Default storage mode
});
// Authenticate (required for cloud storage)
await client.authenticateFromUrl();
// Save data
await client.saveGameData('high_score', 1500);
// Get data
const data = await client.getGameData('high_score');
console.log('High score:', data?.value);
// Delete data
await client.deleteGameData('high_score');
Storage Modes
Comparison
| Feature | Cloud | Local |
|---|---|---|
| Cross-Device Sync | ✅ Yes | ❌ No |
| Requires Authentication | ✅ Yes | ❌ No |
| Works Offline | ❌ No | ✅ Yes |
| Requires game-id | ✅ Yes | ✅ Yes |
| Use Cases | Progress, scores, unlocks | Settings, preferences |
Configuration
Set the default storage mode when creating the client:
// Default to cloud storage
const client = new HyveClient({
storageMode: 'cloud'
});
// Default to local storage
const client = new HyveClient({
storageMode: 'local'
});
Change storage mode at runtime:
// Switch to local storage
client.configureStorage('local');
// Get current mode
const mode = client.getStorageMode(); // 'cloud' or 'local'
Override storage mode per operation:
// Use cloud storage for this operation
await client.saveGameData('score', 1500, 'cloud');
// Use local storage for this operation
await client.saveGameData('settings', {...}, 'local');
Basic Operations
Save Data
Store any JSON-serializable value:
// Numbers
await client.saveGameData('high_score', 1500);
await client.saveGameData('coins', 250);
// Strings
await client.saveGameData('player_name', 'PlayerOne');
await client.saveGameData('current_level', 'forest_temple');
// Booleans
await client.saveGameData('sound_enabled', true);
await client.saveGameData('tutorial_completed', false);
// Arrays
await client.saveGameData('unlocked_levels', [1, 2, 3, 5, 8]);
await client.saveGameData('inventory_items', ['sword', 'shield', 'potion']);
// Objects
await client.saveGameData('player_stats', {
level: 10,
xp: 2500,
health: 100,
mana: 80
});
// Nested objects
await client.saveGameData('game_progress', {
current_chapter: 3,
completed_quests: ['tutorial', 'first_boss', 'hidden_treasure'],
active_quests: [
{ id: 'dragon_slayer', progress: 0.5 },
{ id: 'collect_herbs', progress: 0.8 }
],
achievements: {
speedrun: { unlocked: true, date: '2024-01-15' },
perfectionist: { unlocked: false, progress: 0.6 }
}
});
Get Data
Retrieve saved data with metadata:
const data = await client.getGameData('high_score');
if (data) {
console.log('Value:', data.value); // 1500
console.log('Key:', data.key); // 'high_score'
console.log('Created:', data.created_at); // '2024-01-15T10:30:00Z'
console.log('Updated:', data.updated_at); // '2024-01-15T14:20:00Z'
} else {
console.log('No data found for key: high_score');
}
Response Structure:
interface GameDataItem {
key: string; // Data key
value: GameDataValue; // Your saved value (any JSON type)
created_at: string; // ISO 8601 timestamp
updated_at: string; // ISO 8601 timestamp
}
type GameDataValue =
| string
| number
| boolean
| null
| GameDataValue[]
| { [key: string]: GameDataValue };
Delete Data
Remove data when no longer needed:
// Delete single item
const deleted = await client.deleteGameData('high_score');
if (deleted) {
console.log('High score deleted');
} else {
console.log('High score not found');
}
// Delete with storage mode
await client.deleteGameData('settings', 'local');
Batch Operations
Efficiently handle multiple items at once (recommended for performance):
Batch Save
Save up to 100 items in a single request:
await client.batchSaveGameData([
{ key: 'level_1_score', value: 1000 },
{ key: 'level_2_score', value: 1500 },
{ key: 'level_3_score', value: 2000 },
{ key: 'level_1_stars', value: 3 },
{ key: 'level_2_stars', value: 2 },
{ key: 'level_3_stars', value: 3 }
]);
// With storage mode
await client.batchSaveGameData([
{ key: 'sound_volume', value: 0.8 },
{ key: 'music_volume', value: 0.6 },
{ key: 'graphics_quality', value: 'high' }
], 'local');
Batch Get
Retrieve multiple items at once:
const items = await client.getMultipleGameData([
'level_1_score',
'level_2_score',
'level_3_score'
]);
// Process results
items.forEach(item => {
console.log(`${item.key}: ${item.value} (updated: ${item.updated_at})`);
});
// Map to object
const scores = items.reduce((acc, item) => {
acc[item.key] = item.value;
return acc;
}, {});
console.log(scores);
// { level_1_score: 1000, level_2_score: 1500, level_3_score: 2000 }
Batch Delete
Delete multiple items at once:
const deletedCount = await client.deleteMultipleGameData([
'level_1_score',
'level_2_score',
'level_3_score',
'level_1_stars',
'level_2_stars',
'level_3_stars'
]);
console.log(`Deleted ${deletedCount} items`);
// With storage mode
await client.deleteMultipleGameData([
'old_setting_1',
'old_setting_2'
], 'local');
Complete Examples
Example 1: Game Progress Manager
class ProgressManager {
constructor(client) {
this.client = client;
this.progress = null;
}
async initialize() {
// Load progress from cloud
const data = await this.client.getGameData('game_progress', 'cloud');
if (data) {
this.progress = data.value;
} else {
// Initialize new progress
this.progress = {
level: 1,
xp: 0,
coins: 0,
unlocked_levels: [1],
achievements: []
};
await this.save();
}
}
async save() {
await this.client.saveGameData('game_progress', this.progress, 'cloud');
}
async completeLevel(levelNum, score, stars) {
// Update progress
this.progress.level = Math.max(this.progress.level, levelNum + 1);
this.progress.xp += score;
this.progress.coins += stars * 10;
if (!this.progress.unlocked_levels.includes(levelNum + 1)) {
this.progress.unlocked_levels.push(levelNum + 1);
}
// Save to cloud
await this.save();
// Also save level-specific data
await this.client.batchSaveGameData([
{ key: `level_${levelNum}_score`, value: score },
{ key: `level_${levelNum}_stars`, value: stars },
{ key: `level_${levelNum}_completed_at`, value: new Date().toISOString() }
], 'cloud');
}
async reset() {
// Delete all progress
const keys = [
'game_progress',
...this.progress.unlocked_levels.map(l => `level_${l}_score`),
...this.progress.unlocked_levels.map(l => `level_${l}_stars`)
];
await this.client.deleteMultipleGameData(keys, 'cloud');
// Reinitialize
await this.initialize();
}
}
// Usage
const progress = new ProgressManager(client);
await progress.initialize();
await progress.completeLevel(1, 1500, 3);
Example 2: Settings Manager
class SettingsManager {
constructor(client) {
this.client = client;
this.settings = {
sound: {
enabled: true,
volume: 0.8,
music_volume: 0.6
},
graphics: {
quality: 'high',
fullscreen: false,
vsync: true
},
controls: {
jump: 'space',
move: 'wasd',
sensitivity: 0.5
}
};
}
async load() {
// Load from local storage (works offline)
const data = await this.client.getGameData('settings', 'local');
if (data) {
this.settings = { ...this.settings, ...data.value };
}
this.apply();
}
async save() {
// Save to local storage
await this.client.saveGameData('settings', this.settings, 'local');
}
async updateSound(enabled, volume, musicVolume) {
this.settings.sound = {
enabled,
volume,
music_volume: musicVolume
};
await this.save();
this.apply();
}
async updateGraphics(quality, fullscreen, vsync) {
this.settings.graphics = { quality, fullscreen, vsync };
await this.save();
this.apply();
}
apply() {
// Apply settings to game
// ... implementation ...
}
async reset() {
await this.client.deleteGameData('settings', 'local');
this.settings = this.getDefaults();
await this.save();
}
getDefaults() {
return {
sound: { enabled: true, volume: 0.8, music_volume: 0.6 },
graphics: { quality: 'high', fullscreen: false, vsync: true },
controls: { jump: 'space', move: 'wasd', sensitivity: 0.5 }
};
}
}
// Usage
const settings = new SettingsManager(client);
await settings.load();
await settings.updateSound(true, 0.9, 0.7);
Example 3: High Score Leaderboard
class LeaderboardManager {
constructor(client, levelCount) {
this.client = client;
this.levelCount = levelCount;
}
async saveScore(level, score, stars) {
// Get current high score
const currentData = await this.client.getGameData(`level_${level}_score`, 'cloud');
const currentHighScore = currentData?.value || 0;
// Only save if new high score
if (score > currentHighScore) {
await this.client.batchSaveGameData([
{ key: `level_${level}_score`, value: score },
{ key: `level_${level}_stars`, value: stars },
{ key: `level_${level}_date`, value: new Date().toISOString() }
], 'cloud');
return true; // New high score!
}
return false;
}
async getAllScores() {
// Get all level scores at once
const keys = [];
for (let i = 1; i <= this.levelCount; i++) {
keys.push(`level_${i}_score`, `level_${i}_stars`);
}
const items = await this.client.getMultipleGameData(keys, 'cloud');
// Organize by level
const scores = {};
items.forEach(item => {
const match = item.key.match(/level_(\d+)_(score|stars)/);
if (match) {
const level = match[1];
const type = match[2];
if (!scores[level]) scores[level] = {};
scores[level][type] = item.value;
}
});
return scores;
}
async getTotalScore() {
const scores = await this.getAllScores();
return Object.values(scores).reduce((sum, level) => sum + (level.score || 0), 0);
}
async getTotalStars() {
const scores = await this.getAllScores();
return Object.values(scores).reduce((sum, level) => sum + (level.stars || 0), 0);
}
async resetLevel(level) {
await this.client.deleteMultipleGameData([
`level_${level}_score`,
`level_${level}_stars`,
`level_${level}_date`
], 'cloud');
}
async resetAll() {
const keys = [];
for (let i = 1; i <= this.levelCount; i++) {
keys.push(`level_${i}_score`, `level_${i}_stars`, `level_${i}_date`);
}
await this.client.deleteMultipleGameData(keys, 'cloud');
}
}
// Usage
const leaderboard = new LeaderboardManager(client, 10);
const isHighScore = await leaderboard.saveScore(1, 1500, 3);
const totalScore = await leaderboard.getTotalScore();
const totalStars = await leaderboard.getTotalStars();
Example 4: Mixed Storage Strategy
class GameDataManager {
constructor(client) {
this.client = client;
}
async initialize() {
// Load cloud progress (cross-device sync)
const progressData = await this.client.getGameData('progress', 'cloud');
this.progress = progressData?.value || this.getDefaultProgress();
// Load local settings (device-specific)
const settingsData = await this.client.getGameData('settings', 'local');
this.settings = settingsData?.value || this.getDefaultSettings();
// Load local cache (temporary data)
const cacheData = await this.client.getGameData('cache', 'local');
this.cache = cacheData?.value || {};
}
async saveProgress(data) {
this.progress = { ...this.progress, ...data };
await this.client.saveGameData('progress', this.progress, 'cloud');
}
async saveSettings(data) {
this.settings = { ...this.settings, ...data };
await this.client.saveGameData('settings', this.settings, 'local');
}
async updateCache(key, value) {
this.cache[key] = value;
await this.client.saveGameData('cache', this.cache, 'local');
}
async clearCache() {
this.cache = {};
await this.client.deleteGameData('cache', 'local');
}
getDefaultProgress() {
return {
level: 1,
xp: 0,
coins: 0,
unlocked_content: []
};
}
getDefaultSettings() {
return {
sound_volume: 0.8,
music_volume: 0.6,
graphics_quality: 'high'
};
}
}
// Usage
const gameData = new GameDataManager(client);
await gameData.initialize();
// Save progress to cloud (syncs across devices)
await gameData.saveProgress({ level: 5, xp: 1200 });
// Save settings locally (device-specific)
await gameData.saveSettings({ sound_volume: 0.9 });
// Cache temporary data locally
await gameData.updateCache('tutorial_shown', true);
Storage Limits and Best Practices
Limits
- Key Length: Maximum 255 characters
- Value Size: Maximum 1MB per item
- Batch Size: Maximum 100 items per batch operation
- Key Format: Alphanumeric, underscores, hyphens (case-sensitive)
Best Practices
1. Use Descriptive Keys
// Good
await client.saveGameData('player_level', 10);
await client.saveGameData('level_3_high_score', 2500);
// Avoid
await client.saveGameData('l', 10);
await client.saveGameData('s3', 2500);
2. Batch Related Operations
// Good - Single request
await client.batchSaveGameData([
{ key: 'level_1_score', value: 1000 },
{ key: 'level_1_stars', value: 3 },
{ key: 'level_1_time', value: 120 }
]);
// Avoid - Multiple requests
await client.saveGameData('level_1_score', 1000);
await client.saveGameData('level_1_stars', 3);
await client.saveGameData('level_1_time', 120);
3. Use Appropriate Storage Mode
// Cloud: Data that syncs across devices
await client.saveGameData('unlocked_levels', [1, 2, 3], 'cloud');
await client.saveGameData('achievements', ['speedrun'], 'cloud');
// Local: Device-specific or offline data
await client.saveGameData('graphics_quality', 'high', 'local');
await client.saveGameData('control_scheme', 'keyboard', 'local');
4. Handle Errors Gracefully
async function saveWithRetry(key, value, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
await client.saveGameData(key, value);
return true;
} catch (error) {
console.error(`Save attempt ${i + 1} failed:`, error);
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
return false;
}
5. Validate Data Before Saving
function validateScore(score) {
return typeof score === 'number' && score >= 0 && score < 1000000;
}
async function saveScore(score) {
if (!validateScore(score)) {
throw new Error('Invalid score value');
}
await client.saveGameData('high_score', score);
}
6. Use Versioning for Complex Data
const SAVE_VERSION = 2;
async function saveGameState(state) {
const saveData = {
version: SAVE_VERSION,
timestamp: Date.now(),
state: state
};
await client.saveGameData('game_state', saveData);
}
async function loadGameState() {
const data = await client.getGameData('game_state');
if (!data) return null;
// Handle version migrations
if (data.value.version === 1) {
return migrateV1ToV2(data.value.state);
}
return data.value.state;
}
Error Handling
Common Errors
try {
await client.saveGameData('high_score', 1500);
} catch (error) {
if (error.message.includes('game-id is required')) {
console.error('Missing game-id in URL parameters');
// Fallback to local storage or show error to user
} else if (error.message.includes('JWT token')) {
console.error('Authentication required for cloud storage');
// Try local storage instead
await client.saveGameData('high_score', 1500, 'local');
} else if (error.message.includes('Network')) {
console.error('Network error - retrying or using local storage');
// Queue for retry or save locally
} else {
console.error('Unexpected error:', error);
}
}
Fallback to Local Storage
async function saveWithFallback(key, value) {
try {
// Try cloud first
await client.saveGameData(key, value, 'cloud');
return 'cloud';
} catch (error) {
console.warn('Cloud save failed, falling back to local:', error);
// Fallback to local
await client.saveGameData(key, value, 'local');
return 'local';
}
}
Retry Logic
async function retryOperation(operation, maxRetries = 3, delay = 1000) {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
if (i === maxRetries - 1) throw error;
console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
}
}
}
// Usage
await retryOperation(() =>
client.saveGameData('high_score', 1500)
);
TypeScript Support
Type Definitions
import {
HyveClient,
GameDataItem,
GameDataValue,
GameDataBatchItem,
SaveGameDataResponse
} from '@hyve-sdk/js';
// Custom game data types
interface PlayerProgress {
level: number;
xp: number;
coins: number;
unlocked_levels: number[];
}
interface GameSettings {
sound_volume: number;
music_volume: number;
graphics_quality: 'low' | 'medium' | 'high';
}
// Type-safe save
async function saveProgress(client: HyveClient, progress: PlayerProgress) {
await client.saveGameData('progress', progress, 'cloud');
}
// Type-safe load with type guard
async function loadProgress(client: HyveClient): Promise<PlayerProgress | null> {
const data = await client.getGameData('progress', 'cloud');
if (!data) return null;
// Type guard
if (isPlayerProgress(data.value)) {
return data.value;
}
return null;
}
function isPlayerProgress(value: any): value is PlayerProgress {
return (
typeof value === 'object' &&
typeof value.level === 'number' &&
typeof value.xp === 'number' &&
typeof value.coins === 'number' &&
Array.isArray(value.unlocked_levels)
);
}
API Reference
Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
saveGameData | key: string, value: GameDataValue, storage?: 'cloud' | 'local' | Promise<SaveGameDataResponse> | Save single value |
getGameData | key: string, storage?: 'cloud' | 'local' | Promise<GameDataItem | null> | Get single value |
deleteGameData | key: string, storage?: 'cloud' | 'local' | Promise<boolean> | Delete single value |
batchSaveGameData | items: GameDataBatchItem[], storage?: 'cloud' | 'local' | Promise<SaveGameDataResponse> | Save multiple values (max 100) |
getMultipleGameData | keys: string[], storage?: 'cloud' | 'local' | Promise<GameDataItem[]> | Get multiple values |
deleteMultipleGameData | keys: string[], storage?: 'cloud' | 'local' | Promise<number> | Delete multiple values, returns count |
configureStorage | mode: 'cloud' | 'local' | void | Change default storage mode |
getStorageMode | none | 'cloud' | 'local' | Get current storage mode |
Types
type GameDataValue =
| string
| number
| boolean
| null
| GameDataValue[]
| { [key: string]: GameDataValue };
interface GameDataItem {
key: string;
value: GameDataValue;
created_at: string;
updated_at: string;
}
interface GameDataBatchItem {
key: string;
value: GameDataValue;
}
interface SaveGameDataResponse {
success: boolean;
message: string;
}
Troubleshooting
Data Not Saving
// Check authentication
if (!client.hasJwtToken()) {
console.error('No JWT token - cloud storage requires authentication');
}
// Check game-id
if (!client.getGameId()) {
console.error('No game-id in URL parameters');
}
// Try with explicit storage mode
await client.saveGameData('test', 123, 'cloud');
Data Not Loading
const data = await client.getGameData('high_score');
if (!data) {
console.log('Key not found - may not have been saved yet');
// Initialize with default value
await client.saveGameData('high_score', 0);
}
Local Storage Full
try {
await client.saveGameData('large_data', bigObject, 'local');
} catch (error) {
if (error.message.includes('QuotaExceeded')) {
console.error('Local storage full - clear old data');
// Clear cache or old data
await client.deleteMultipleGameData([
'old_cache_1',
'old_cache_2'
], 'local');
}
}