Skip to main content

Data Portability

Export and import achievement data for backups, cross-device sync, and cloud storage integration.

Overview

achievements-engine provides comprehensive data portability:

  • Export achievement data as JSON
  • Import data with merge strategies
  • Cloud storage integration (AWS S3, Azure Blob Storage)
  • Cross-device sync via REST API
  • Backup and restore functionality
  • Configuration hash validation to prevent version mismatches

Basic Export/Import

Export Data

import { AchievementEngine } from 'achievements-engine';

const engine = new AchievementEngine({
achievements,
storage: 'localStorage'
});

// Export all data as JSON string
const jsonData = engine.export();
console.log(jsonData); // JSON string

Exported Format:

{
"version": "1.1.0",
"configHash": "abc123def456",
"timestamp": 1703337000000,
"metrics": {
"score": [100, 150],
"level": [5],
"completedTutorial": [true]
},
"unlockedAchievements": ["score_100", "level_5", "tutorial_complete"]
}

Import Data

import { importAchievementData } from 'achievements-engine';

const result = engine.import(jsonData);

if (result.success) {
console.log('Import successful!');
console.log('Warnings:', result.warnings);
} else {
console.error('Import failed:', result.errors);
}

Export API

Using the Engine

// Export from the engine instance
const data = engine.export();

Using the Utility Function

import { exportAchievementData, createConfigHash } from 'achievements-engine';

const data = exportAchievementData({
metrics: engine.getMetrics(),
unlockedAchievements: engine.getUnlocked(),
achievements // Your achievement config
});

console.log(data); // JSON string

Creating Configuration Hash

The configuration hash helps validate that imported data matches your achievement configuration:

import { createConfigHash } from 'achievements-engine';

const hash = createConfigHash(achievements);
console.log(hash); // "abc123def456"

Import Strategies

Replace Strategy (Default)

Completely replaces existing data:

const result = engine.import(jsonData, {
merge: false,
overwrite: true
});

Use when:

  • Restoring from a known good backup
  • Resetting all achievements
  • Initial data load

Merge Strategy

Combines imported data with existing data:

const result = engine.import(jsonData, {
merge: true,
overwrite: false
});

Merge rules:

  • Unlocked achievements stay unlocked (union)
  • Metrics are combined (arrays merged)
  • Preserves all progress from both sources

Use when:

  • Syncing between devices
  • Importing partial backups
  • Merging multiple saves

Validated Import

Validate configuration hash before importing:

const result = engine.import(jsonData, {
merge: true,
validateConfig: true,
expectedConfigHash: createConfigHash(achievements)
});

if (!result.success) {
console.error('Configuration mismatch!', result.errors);
}

Import Options

ImportOptions Interface

interface ImportOptions {
merge?: boolean; // Combine with existing data (default: false)
overwrite?: boolean; // Overwrite existing data (default: true)
validateConfig?: boolean; // Validate configuration hash (default: false)
expectedConfigHash?: string; // Expected config hash for validation
}

ImportResult Interface

interface ImportResult {
success: boolean;
errors?: string[];
warnings?: string[];
mergedMetrics?: AchievementMetrics;
mergedUnlocked?: string[];
}

Backup to File

Download as JSON File (Browser)

function downloadBackup(engine: AchievementEngine) {
const data = engine.export();
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = url;
link.download = `achievements-backup-${Date.now()}.json`;
link.click();

URL.revokeObjectURL(url);
}

Upload from File (Browser)

function uploadBackup(engine: AchievementEngine, file: File) {
const reader = new FileReader();

reader.onload = async (e) => {
const jsonData = e.target?.result as string;
const result = engine.import(jsonData, { merge: true });

if (result.success) {
console.log('Backup restored!');
if (result.warnings?.length) {
console.warn('Import warnings:', result.warnings);
}
} else {
console.error('Import failed:', result.errors);
}
};

reader.readAsText(file);
}

// Usage
const fileInput = document.getElementById('file-input') as HTMLInputElement;
fileInput.addEventListener('change', (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
uploadBackup(engine, file);
}
});

Cloud Storage Integration

AWS S3 Integration

import { S3 } from 'aws-sdk';
import { AchievementEngine } from 'achievements-engine';

class S3BackupService {
private s3: S3;
private bucket: string;

constructor() {
this.s3 = new S3({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: 'us-east-1'
});
this.bucket = 'my-achievements-backup';
}

async backup(engine: AchievementEngine, userId: string): Promise<{ success: boolean; location?: string; error?: any }> {
const data = engine.export();

const params = {
Bucket: this.bucket,
Key: `users/${userId}/achievements-${Date.now()}.json`,
Body: data,
ContentType: 'application/json',
ServerSideEncryption: 'AES256' // Encrypt at rest
};

try {
const result = await this.s3.putObject(params).promise();
console.log('Backed up to S3');
return { success: true, location: result.ETag };
} catch (error) {
console.error('S3 backup failed:', error);
return { success: false, error };
}
}

async restore(engine: AchievementEngine, backupKey: string): Promise<ImportResult> {
const params = {
Bucket: this.bucket,
Key: backupKey
};

try {
const data = await this.s3.getObject(params).promise();
const result = engine.import(jsonData, { merge: true });
console.log('Restored from S3');
return result;
} catch (error) {
console.error('S3 restore failed:', error);
return { success: false, errors: [error.message] };
}
}

async listBackups(userId: string) {
const params = {
Bucket: this.bucket,
Prefix: `users/${userId}/`
};

const data = await this.s3.listObjectsV2(params).promise();

return data.Contents?.map(item => ({
key: item.Key,
size: item.Size,
lastModified: item.LastModified
})) || [];
}

async deleteOldBackups(userId: string, keepCount: number = 10) {
const backups = await this.listBackups(userId);

if (backups.length > keepCount) {
const toDelete = backups
.sort((a, b) => (b.lastModified?.getTime() || 0) - (a.lastModified?.getTime() || 0))
.slice(keepCount);

for (const backup of toDelete) {
await this.s3.deleteObject({
Bucket: this.bucket,
Key: backup.key!
}).promise();
}
}
}
}

Usage

const backupService = new S3BackupService();

// Backup
await backupService.backup(engine, 'user123');

// Restore
await backupService.restore(engine, 'users/user123/achievements-1703337000000.json');

// List backups
const backups = await backupService.listBackups('user123');

// Prune old backups
await backupService.deleteOldBackups('user123', 10);

Azure Blob Storage Integration

import { BlobServiceClient } from '@azure/storage-blob';
import { AchievementEngine } from 'achievements-engine';

class AzureBackupService {
private containerClient;

constructor() {
const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING!;
const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
this.containerClient = blobServiceClient.getContainerClient('achievements-backup');
}

async backup(engine: AchievementEngine, userId: string) {
const data = engine.export();
const blobName = `users/${userId}/achievements-${Date.now()}.json`;
const blockBlobClient = this.containerClient.getBlockBlobClient(blobName);

try {
await blockBlobClient.upload(data, data.length, {
blobHTTPHeaders: { blobContentType: 'application/json' }
});

console.log('Backed up to Azure Blob Storage');
return { success: true, blobName };
} catch (error) {
console.error('Azure backup failed:', error);
return { success: false, error };
}
}

async restore(engine: AchievementEngine, blobName: string) {
const blockBlobClient = this.containerClient.getBlockBlobClient(blobName);

try {
const downloadResponse = await blockBlobClient.download(0);
const jsonData = await this.streamToString(downloadResponse.readableStreamBody!);

const result = engine.import(jsonData, { merge: true });
console.log('Restored from Azure');
return result;
} catch (error) {
console.error('Azure restore failed:', error);
return { success: false, errors: [error.message] };
}
}

private async streamToString(readableStream: NodeJS.ReadableStream): Promise<string> {
return new Promise((resolve, reject) => {
const chunks: string[] = [];
readableStream.on('data', (data) => {
chunks.push(data.toString());
});
readableStream.on('end', () => {
resolve(chunks.join(''));
});
readableStream.on('error', reject);
});
}
}

Automatic Backup Strategies

Backup on Achievement Unlock

const engine = new AchievementEngine({
achievements,
storage: 'localStorage'
});

engine.on('achievement:unlocked', async (event) => {
// Backup to cloud whenever an achievement unlocks
await backupService.backup(engine, userId);
console.log(`Backed up after unlocking: ${event.achievementTitle}`);
});

Periodic Auto-Backup

// Backup every 5 minutes
const backupInterval = setInterval(async () => {
await backupService.backup(engine, userId);
console.log('Auto-backup completed');
}, 5 * 60 * 1000);

// Cleanup on app shutdown
process.on('SIGINT', () => {
clearInterval(backupInterval);
process.exit();
});

Backup on State Changes

engine.on('state:changed', async (event) => {
// Backup whenever metrics or achievements change
await backupService.backup(engine, userId);
});

Cross-Device Sync with REST API

Use REST API storage for automatic cross-device synchronization:

import { AchievementEngine, RestApiStorage } from 'achievements-engine';

const engine = new AchievementEngine({
achievements,
storage: 'restApi',
restApiConfig: {
baseUrl: 'https://api.example.com/achievements',
headers: {
'Authorization': `Bearer ${userToken}`,
'X-User-ID': userId
}
}
});

The REST API storage automatically:

  • Syncs on every update
  • Pulls latest data on initialization
  • Handles server-side conflict resolution

Best Practices

1. Encrypt Sensitive Data

import CryptoJS from 'crypto-js';

function encryptData(data: string, secret: string): string {
return CryptoJS.AES.encrypt(data, secret).toString();
}

function decryptData(encryptedData: string, secret: string): string {
const bytes = CryptoJS.AES.decrypt(encryptedData, secret);
return bytes.toString(CryptoJS.enc.Utf8);
}

// Export with encryption
const data = engine.export();
const encrypted = encryptData(data, userSecret);
await backupService.backupEncrypted(encrypted, userId);

// Import with decryption
const encrypted = await backupService.restoreEncrypted(backupKey);
const decrypted = decryptData(encrypted, userSecret);
engine.import(decrypted);

2. Version Your Backups

const APP_VERSION = '1.1.0';
const blobName = `users/${userId}/v${APP_VERSION}/achievements-${Date.now()}.json`;

3. Validate Before Import

import { createConfigHash } from 'achievements-engine';

const expectedHash = createConfigHash(achievements);

const result = engine.import(jsonData, {
merge: true,
validateConfig: true,
expectedConfigHash: expectedHash
});

if (!result.success) {
console.error('Configuration mismatch!', result.errors);
// Don't import - achievement definitions have changed
} else {
console.log('Import successful with valid configuration');
}

4. Handle Import Errors Gracefully

async function safeImport(engine: AchievementEngine, jsonData: string) {
try {
const result = engine.import(jsonData, { merge: true });

if (!result.success) {
console.error('Import failed:', result.errors);
return false;
}

if (result.warnings?.length) {
console.warn('Import warnings:', result.warnings);
}

console.log('Import successful');
return true;
} catch (error) {
console.error('Import error:', error);
return false;
}
}

5. Implement Backup Retention

class BackupManager {
async backupWithRetention(
engine: AchievementEngine,
userId: string,
maxBackups: number = 10
) {
// Create new backup
await backupService.backup(engine, userId);

// Prune old backups
await backupService.deleteOldBackups(userId, maxBackups);
}
}

Complete Backup System Example

import { AchievementEngine } from 'achievements-engine';

class AchievementBackupManager {
private engine: AchievementEngine;
private backupService: S3BackupService;
private userId: string;
private autoBackupInterval?: NodeJS.Timeout;

constructor(engine: AchievementEngine, userId: string) {
this.engine = engine;
this.backupService = new S3BackupService();
this.userId = userId;
this.setupAutoBackup();
}

private setupAutoBackup() {
// Backup on achievement unlock
this.engine.on('achievement:unlocked', () => this.backup());

// Periodic backup every 5 minutes
this.autoBackupInterval = setInterval(() => this.backup(), 5 * 60 * 1000);
}

async backup() {
try {
const result = await this.backupService.backup(this.engine, this.userId);
if (result.success) {
console.log('Backup successful');
await this.backupService.deleteOldBackups(this.userId, 10);
}
} catch (error) {
console.error('Backup failed:', error);
}
}

async restore(backupKey: string) {
try {
const result = await this.backupService.restore(this.engine, backupKey);
if (result.success) {
console.log('Restore successful');
} else {
console.error('Restore failed:', result.errors);
}
return result;
} catch (error) {
console.error('Restore error:', error);
return { success: false, errors: [error.message] };
}
}

async listBackups() {
return await this.backupService.listBackups(this.userId);
}

destroy() {
if (this.autoBackupInterval) {
clearInterval(this.autoBackupInterval);
}
}
}

// Usage
const backupManager = new AchievementBackupManager(engine, 'user123');

// Manual backup
await backupManager.backup();

// List available backups
const backups = await backupManager.listBackups();

// Restore from backup
await backupManager.restore('users/user123/achievements-1703337000000.json');

// Cleanup on shutdown
process.on('SIGINT', () => {
backupManager.destroy();
process.exit();
});

Next Steps