The persistence module provides a simple key-value store that persists data across app sessions, with optional encryption and LRU caching.
PersistentStore allows you to store binary data or strings that survive app restarts. It supports:
- User-scoped or device-global storage
- Optional encryption for sensitive data
- TTL (time-to-live) for automatic expiration
- LRU cache behavior with maximum weight limits
- Batch writes for performance
Add the persistence module to your BUILD.bazel dependencies:
valdi_module(
name = "my_module",
deps = [
"@valdi//src/valdi_modules/src/valdi/persistence",
],
)import { PersistentStore } from 'persistence/src/PersistentStore';
// Create a persistent store
const store = new PersistentStore('my_app_data');
// Store a string
await store.storeString('username', 'alice');
// Fetch a string
const username = await store.fetchString('username');
console.log(username); // 'alice'
// Store binary data
const binaryData = new Uint8Array([1, 2, 3, 4, 5]);
await store.store('binary_key', binaryData.buffer);
// Fetch binary data
const data = await store.fetch('binary_key');
console.log(new Uint8Array(data)); // [1, 2, 3, 4, 5]
// Remove data
await store.remove('username');constructor(name: string, options?: PersistentStoreOptions)Creates a new persistent store with the given name.
Parameters:
name- Unique identifier for this storeoptions- Optional configuration (see PersistentStoreOptions below)
const store = new PersistentStore('user_preferences', {
enableEncryption: true,
maxWeight: 1024 * 1024 // 1MB cache
});Store binary data with the given key.
Parameters:
key- Unique key for this datavalue- Binary data to storettlSeconds- Optional time-to-live in seconds (data auto-expires)weight- Optional weight for LRU eviction (when maxWeight is set)
const data = new Uint8Array([1, 2, 3, 4]);
await store.store('my_data', data.buffer);
// With TTL (expires in 1 hour)
await store.store('temporary', data.buffer, 3600);
// With weight for LRU cache
await store.store('cached_image', imageBuffer, undefined, 512 * 1024); // 512KBStore a string with the given key.
await store.storeString('user_name', 'Alice');
// With TTL (expires in 24 hours)
await store.storeString('session_token', 'abc123', 86400);Fetch binary data for the given key. Throws if key doesn't exist.
try {
const data = await store.fetch('my_data');
console.log(new Uint8Array(data));
} catch (error) {
console.log('Key not found or expired');
}Fetch a string for the given key. Throws if key doesn't exist.
try {
const username = await store.fetchString('user_name');
console.log(username);
} catch (error) {
console.log('Key not found or expired');
}Test whether data for the given key exists (and hasn't expired).
if (await store.exists('user_name')) {
const username = await store.fetchString('user_name');
console.log(username);
}Remove data for the given key.
await store.remove('user_name');Remove all data from this persistent store.
// Clear all stored data
await store.removeAll();Fetch all items as a PropertyList (map of key-value pairs).
const allData = await store.fetchAll();
console.log(allData);Configuration options for creating a persistent store.
interface PersistentStoreOptions {
disableBatchWrites?: boolean;
deviceGlobal?: boolean;
maxWeight?: number;
enableEncryption?: boolean;
}By default, save operations are batched to minimize disk I/O. Set to true to write immediately on every store() or remove() call.
const store = new PersistentStore('immediate_writes', {
disableBatchWrites: true // Writes happen immediately
});Default: false (batching enabled for better performance)
Whether the store should be available globally across all user sessions, instead of being scoped to the current user. Data will not be encrypted when this flag is set.
// Device-global store (shared across all users)
const deviceStore = new PersistentStore('device_settings', {
deviceGlobal: true
});
// User-scoped store (default)
const userStore = new PersistentStore('user_preferences');Default: false (user-scoped)
If set, the store acts like an LRU cache where items are removed as needed to keep total weight below this value. When using this, provide weight when calling store().
const imageCache = new PersistentStore('image_cache', {
maxWeight: 10 * 1024 * 1024 // 10MB max
});
// Store with weight
const imageData = await loadImage();
await imageCache.store('image_123', imageData, undefined, 2 * 1024 * 1024); // 2MBDefault: 0 (no weight limit)
Set to true when storing sensitive data (credit cards, secret keys, authentication cookies).
Performance Note: Encryption has some performance overhead. Use false for non-sensitive data.
const secureStore = new PersistentStore('credentials', {
enableEncryption: true
});
await secureStore.storeString('api_key', 'secret_key_123');Default: false (no encryption)
class UserPreferences {
private store = new PersistentStore('user_prefs');
async saveTheme(theme: 'light' | 'dark') {
await this.store.storeString('theme', theme);
}
async getTheme(): Promise<'light' | 'dark'> {
try {
return await this.store.fetchString('theme') as 'light' | 'dark';
} catch {
return 'light'; // Default
}
}
}class SessionManager {
private store = new PersistentStore('sessions', {
enableEncryption: true
});
async saveSession(token: string) {
// Expire after 7 days
const ttl = 7 * 24 * 60 * 60;
await this.store.storeString('auth_token', token, ttl);
}
async getSession(): Promise<string | null> {
if (await this.store.exists('auth_token')) {
return await this.store.fetchString('auth_token');
}
return null;
}
async clearSession() {
await this.store.remove('auth_token');
}
}class ImageCache {
private store = new PersistentStore('images', {
maxWeight: 50 * 1024 * 1024 // 50MB cache
});
async cacheImage(url: string, imageData: ArrayBuffer) {
const weight = imageData.byteLength;
// Cache for 24 hours
await this.store.store(url, imageData, 86400, weight);
}
async getImage(url: string): Promise<ArrayBuffer | null> {
if (await this.store.exists(url)) {
return await this.store.fetch(url);
}
return null;
}
}async function storeJSON(store: PersistentStore, key: string, data: any) {
const jsonString = JSON.stringify(data);
await store.storeString(key, jsonString);
}
async function fetchJSON<T>(store: PersistentStore, key: string): Promise<T | null> {
try {
const jsonString = await store.fetchString(key);
return JSON.parse(jsonString);
} catch {
return null;
}
}
// Usage
const store = new PersistentStore('app_data');
await storeJSON(store, 'user', { name: 'Alice', age: 30 });
const user = await fetchJSON<{ name: string; age: number }>(store, 'user');-
Use encryption for sensitive data: Always set
enableEncryption: truefor passwords, tokens, and personal information. -
Choose appropriate TTL: Set reasonable expiration times to avoid stale data and save storage space.
-
User-scoped by default: Unless you explicitly need device-global storage, keep the default user-scoped behavior.
-
Batch writes for performance: Keep the default batch write behavior unless you need guaranteed immediate persistence.
-
Handle missing keys: Always wrap
fetch()andfetchString()in try-catch blocks or check withexists()first. -
Use weights for caches: When using
maxWeight, always provide meaningful weights tostore()for proper LRU behavior. -
Store name uniqueness: Use unique store names to avoid conflicts between different parts of your application.
async function safeGetString(store: PersistentStore, key: string, defaultValue: string): Promise<string> {
try {
return await store.fetchString(key);
} catch (error) {
console.warn(`Failed to fetch ${key}:`, error);
return defaultValue;
}
}
async function safeStore(store: PersistentStore, key: string, value: string): Promise<boolean> {
try {
await store.storeString(key, value);
return true;
} catch (error) {
console.error(`Failed to store ${key}:`, error);
return false;
}
}import { Component } from 'valdi_core/src/Component';
import { PersistentStore } from 'persistence/src/PersistentStore';
export class SettingsComponent extends Component {
private store = new PersistentStore('settings');
async onCreate() {
// Load saved settings
const theme = await this.loadTheme();
this.setState({ theme });
}
private async loadTheme(): Promise<string> {
try {
return await this.store.fetchString('theme');
} catch {
return 'light'; // Default
}
}
private async saveTheme(theme: string) {
await this.store.storeString('theme', theme);
this.setState({ theme });
}
onRender() {
// Render settings UI...
}
}The persistence module works on:
- ✅ iOS (uses file system storage)
- ✅ Android (uses file system storage)
⚠️ Web (requires polyfill or alternative implementation)
- Core Utilities - LRU Cache and other utilities
- File System - Lower-level file operations
- Component Lifecycle - Loading data in components
- RxJS - Reactive data patterns
- Batch writes are enabled by default for better performance
- Encryption has overhead - only use for sensitive data
- LRU caching helps - use maxWeight to limit storage usage
- TTL prevents bloat - set reasonable expiration times