Storage adapters provide a pluggable interface for persisting achievement state. The core package includes two built-in adapters: localStorageAdapter for browser localStorage, and inMemoryAdapter for testing or ephemeral state.
StorageAdapter Interface
type StorageAdapter = {
get(key: string): string | null;
set(key: string, value: string): void;
remove(key: string): void;
}
All storage operations use string keys and values. The engine handles serialization/deserialization internally.
get
(key: string) => string | null
required
Retrieve a value by key. Returns null if not found.
set
(key: string, value: string) => void
required
Store a value with the given key. Overwrites existing value.
remove
(key: string) => void
required
Delete a value by key. No-op if key doesn’t exist.
localStorageAdapter
function localStorageAdapter(prefix?: string): StorageAdapter
Adapter that uses browser localStorage for persistent storage. This is the default adapter used by createAchievements().
Optional prefix for all storage keys. Useful for namespacing when multiple achievement systems share the same localStorage.
Storage adapter instance that reads/writes from localStorage
Example
import { createAchievements, localStorageAdapter } from '@achievements-manager/core';
const engine = createAchievements({
definitions,
storage: localStorageAdapter('game'), // Keys: "game:unlocked", "game:progress", etc.
});
Behavior
- Server-safe: Returns null/no-ops if
window is undefined (SSR)
- Error handling: Catches and ignores storage quota errors
- Prefix format:
prefix:key (e.g., game:unlocked)
- Default prefix: No prefix if not provided
Storage Keys
With prefix "game", the adapter creates:
game:unlocked - Unlocked achievement IDs (JSON array)
game:unlocked:hash - Integrity hash for unlocked data
game:progress - Progress values (JSON object)
game:progress:hash - Integrity hash for progress data
game:items - Collected items (JSON object of arrays)
game:items:hash - Integrity hash for items data
Implementation
export function localStorageAdapter(prefix?: string): StorageAdapter {
const k = (key: string) => (prefix ? `${prefix}:${key}` : key);
return {
get(key) {
if (typeof window === 'undefined') return null;
try {
return localStorage.getItem(k(key));
} catch {
return null;
}
},
set(key, value) {
if (typeof window === 'undefined') return;
try {
localStorage.setItem(k(key), value);
} catch {
// Storage unavailable or full — silently ignore
}
},
remove(key) {
if (typeof window === 'undefined') return;
try {
localStorage.removeItem(k(key));
} catch {
// Storage unavailable — silently ignore
}
},
};
}
inMemoryAdapter
function inMemoryAdapter(): StorageAdapter
Adapter that stores data in memory using a Map. Data is lost on page reload. Useful for testing, demos, or temporary achievement tracking.
Storage adapter instance that stores data in memory
Example
import { createAchievements, inMemoryAdapter } from '@achievements-manager/core';
const engine = createAchievements({
definitions,
storage: inMemoryAdapter(), // No persistence
});
Behavior
- Ephemeral: Data is lost on page reload
- Isolated: Each adapter instance has its own Map
- Fast: No I/O overhead
- Testing-friendly: No localStorage cleanup needed
Implementation
export function inMemoryAdapter(): StorageAdapter {
const store = new Map<string, string>();
return {
get: (key) => store.get(key) ?? null,
set: (key, value) => {
store.set(key, value);
},
remove: (key) => {
store.delete(key);
},
};
}
Custom Storage Adapters
You can create custom adapters for other storage backends:
IndexedDB Example
import { StorageAdapter } from '@achievements-manager/core';
function indexedDBAdapter(dbName: string): StorageAdapter {
// Note: This is a simplified example. Production code should handle
// async operations properly, possibly with a synchronous cache layer.
const cache = new Map<string, string>();
// Initialize from IndexedDB on creation
// ... (omitted for brevity)
return {
get: (key) => cache.get(key) ?? null,
set: (key, value) => {
cache.set(key, value);
// Write to IndexedDB asynchronously
// ... (omitted for brevity)
},
remove: (key) => {
cache.delete(key);
// Remove from IndexedDB asynchronously
// ... (omitted for brevity)
},
};
}
Cookie Storage Example
import { StorageAdapter } from '@achievements-manager/core';
function cookieAdapter(prefix: string): StorageAdapter {
return {
get: (key) => {
const name = `${prefix}_${key}=`;
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.startsWith(name)) {
return decodeURIComponent(cookie.substring(name.length));
}
}
return null;
},
set: (key, value) => {
const encoded = encodeURIComponent(value);
document.cookie = `${prefix}_${key}=${encoded}; path=/; max-age=31536000`;
},
remove: (key) => {
document.cookie = `${prefix}_${key}=; path=/; max-age=0`;
},
};
}
Remote Storage Example
import { StorageAdapter } from '@achievements-manager/core';
function remoteAdapter(apiUrl: string, userId: string): StorageAdapter {
const cache = new Map<string, string>();
// Hydrate cache from server on initialization
fetch(`${apiUrl}/achievements/${userId}`)
.then(res => res.json())
.then(data => {
Object.entries(data).forEach(([key, value]) => {
cache.set(key, value as string);
});
});
return {
get: (key) => cache.get(key) ?? null,
set: (key, value) => {
cache.set(key, value);
// Sync to server
fetch(`${apiUrl}/achievements/${userId}`, {
method: 'PUT',
body: JSON.stringify({ [key]: value }),
});
},
remove: (key) => {
cache.delete(key);
// Sync to server
fetch(`${apiUrl}/achievements/${userId}/${key}`, {
method: 'DELETE',
});
},
};
}
Best Practices
Synchronous Operations
The StorageAdapter interface is synchronous. If your backend is async (like IndexedDB or network requests), maintain a synchronous in-memory cache and sync asynchronously in the background.
Error Handling
Storage operations should be resilient:
get() should return null on errors
set() and remove() should fail silently
- The engine will continue to work with in-memory state
Testing
Use inMemoryAdapter() in tests to avoid localStorage cleanup:
import { createAchievements, inMemoryAdapter } from '@achievements-manager/core';
describe('achievements', () => {
it('should unlock achievements', () => {
const engine = createAchievements({
definitions,
storage: inMemoryAdapter(), // Clean state for each test
});
engine.unlock('first-login');
expect(engine.isUnlocked('first-login')).toBe(true);
});
});
See Also