RunCache provides robust persistent storage capabilities that allow your cache data to survive application restarts or browser refreshes. This guide explains how to use persistent storage effectively in your applications.
Understanding Persistent Storage
By default, RunCache stores all data in memory, which means cache entries are lost when:
Your Node.js application restarts
The user refreshes or closes their browser
The device loses power
Persistent storage solves this problem by saving cache data to a more durable medium:
File system (for Node.js applications)
localStorage or IndexedDB (for browser applications)
Custom storage backends (for specialized needs)
Storage Adapters
RunCache uses storage adapters to abstract the details of different storage mechanisms. Three built-in adapters are provided:
LocalStorageAdapter: For browser environments, uses the browser's localStorage API
IndexedDBAdapter: For browser environments, uses the browser's IndexedDB API for larger datasets
FilesystemAdapter: For Node.js environments, stores cache data in the filesystem
Basic Setup
Configuring a Storage Adapter
To enable persistent storage, configure RunCache with a storage adapter:
import { RunCache, FilesystemAdapter } from 'run-cache';
// For Node.js applications
RunCache.configure({
storageAdapter: new FilesystemAdapter({
storageKey: 'my-app-cache',
autoSaveInterval: 300000 // 5 minutes
})
});
// For browser applications with small data
RunCache.configure({
storageAdapter: new LocalStorageAdapter({
storageKey: 'my-app-cache',
autoSaveInterval: 300000 // 5 minutes
})
});
// For browser applications with larger data
RunCache.configure({
storageAdapter: new IndexedDBAdapter({
storageKey: 'my-app-cache',
autoSaveInterval: 300000 // 5 minutes
})
});
Manual Save and Load
You can manually trigger save and load operations:
// Manually save cache state
await RunCache.saveToStorage();
// Manually load cache state
await RunCache.loadFromStorage();
Auto-Save Configuration
Configure automatic saving at regular intervals:
// Save cache to storage every 5 minutes
RunCache.setupAutoSave(300000);
// Disable auto-saving
RunCache.setupAutoSave(0);
Storage Adapters in Detail
FilesystemAdapter
The FilesystemAdapter stores cache data in the filesystem. This adapter is suitable for Node.js applications.
Constructor Options
interface FilesystemAdapterConfig {
// Storage key/filename to use (default: "run-cache-data")
storageKey?: string;
// Auto-save interval in milliseconds (default: 0, disabled)
autoSaveInterval?: number;
// Whether to load cache automatically when adapter is initialized (default: true)
autoLoadOnInit?: boolean;
// Custom file path (default: current working directory)
filePath?: string;
}
Example Usage
import { RunCache, FilesystemAdapter } from 'run-cache';
import path from 'path';
// Basic usage
RunCache.configure({
storageAdapter: new FilesystemAdapter()
});
// With custom options
RunCache.configure({
storageAdapter: new FilesystemAdapter({
storageKey: 'my-app-cache',
filePath: path.join(process.cwd(), 'cache'),
autoSaveInterval: 300000, // 5 minutes
autoLoadOnInit: true
})
});
Storage Format
The FilesystemAdapter stores data in a JSON file with the following structure:
The LocalStorageAdapter uses the browser's localStorage API to persist cache data. This adapter is suitable for small to medium-sized cache data in web applications.
Constructor Options
interface StorageAdapterConfig {
// Key to use in localStorage (default: "run-cache-data")
storageKey?: string;
// Auto-save interval in milliseconds (default: 0, disabled)
autoSaveInterval?: number;
// Whether to load cache automatically when adapter is initialized (default: true)
autoLoadOnInit?: boolean;
}
Example Usage
import { RunCache, LocalStorageAdapter } from 'run-cache';
// Basic usage
RunCache.configure({
storageAdapter: new LocalStorageAdapter()
});
// With custom options
RunCache.configure({
storageAdapter: new LocalStorageAdapter({
storageKey: 'my-app-cache',
autoSaveInterval: 60000, // 1 minute
autoLoadOnInit: true
})
});
Limitations
Limited to approximately 5-10MB of data (varies by browser)
Synchronous API that can block the main thread with large data
Only supports string data
IndexedDBAdapter
The IndexedDBAdapter uses the browser's IndexedDB API to persist cache data. This adapter is suitable for larger datasets in web applications.
Constructor Options
interface StorageAdapterConfig {
// Database and store name (default: "run-cache-data")
storageKey?: string;
// Auto-save interval in milliseconds (default: 0, disabled)
autoSaveInterval?: number;
// Whether to load cache automatically when adapter is initialized (default: true)
autoLoadOnInit?: boolean;
}
Example Usage
import { RunCache, IndexedDBAdapter } from 'run-cache';
// Basic usage
RunCache.configure({
storageAdapter: new IndexedDBAdapter()
});
// With custom options
RunCache.configure({
storageAdapter: new IndexedDBAdapter({
storageKey: 'my-app-cache',
autoSaveInterval: 300000, // 5 minutes
autoLoadOnInit: true
})
});
Advantages
Supports much larger data sizes (typically 50-100MB or more)
Asynchronous API that doesn't block the main thread
Better performance with large datasets
Limitations
More complex API
Not available in all browser contexts (e.g., some private browsing modes)
Custom Storage Adapters
You can create your own storage adapter by implementing the StorageAdapter interface:
Here's an example of a custom adapter that uses AWS S3 for storage:
import { StorageAdapter } from 'run-cache';
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
class S3StorageAdapter implements StorageAdapter {
private s3: S3Client;
private bucket: string;
private key: string;
constructor(options: { bucket: string, key?: string, region?: string }) {
this.s3 = new S3Client({ region: options.region || 'us-east-1' });
this.bucket = options.bucket;
this.key = options.key || 'run-cache-data.json';
}
async save(data: string): Promise<void> {
try {
const command = new PutObjectCommand({
Bucket: this.bucket,
Key: this.key,
Body: data,
ContentType: 'application/json'
});
await this.s3.send(command);
} catch (error) {
console.error('Failed to save cache to S3:', error);
throw error;
}
}
async load(): Promise<string | null> {
try {
const command = new GetObjectCommand({
Bucket: this.bucket,
Key: this.key
});
const response = await this.s3.send(command);
const body = await response.Body?.transformToString();
return body || null;
} catch (error) {
// If the object doesn't exist, return null instead of throwing
if ((error as any).name === 'NoSuchKey') {
return null;
}
console.error('Failed to load cache from S3:', error);
return null;
}
}
}
// Usage
RunCache.configure({
storageAdapter: new S3StorageAdapter({
bucket: 'my-cache-bucket',
key: 'my-app-cache.json',
region: 'us-west-2'
})
});
Advanced Usage
Selective Persistence
You might want to persist only certain types of cache entries. You can achieve this using middleware:
// Add middleware to control what gets persisted
RunCache.use(async (value, context, next) => {
// First, pass through to the actual cache operation
const result = await next(value);
// For set operations, check if we should persist this entry
if (context.operation === 'set' && context.key.startsWith('temp:')) {
// Mark this entry as non-persistent
context.metadata = {
...context.metadata,
persistent: false
};
}
return result;
});
// Add middleware to filter out non-persistent entries during save
RunCache.use(async (value, context, next) => {
if (context.operation === 'save' && value) {
// Parse the cache state
const cacheState = JSON.parse(value);
// Filter out non-persistent entries
const filteredEntries = {};
for (const [key, entry] of Object.entries(cacheState.entries)) {
if (entry.metadata?.persistent !== false) {
filteredEntries[key] = entry;
}
}
// Update the cache state
cacheState.entries = filteredEntries;
// Pass the filtered state to the next middleware
return next(JSON.stringify(cacheState));
}
return next(value);
});
Encryption
You might want to encrypt sensitive data before persisting it:
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
// Encryption key and IV (in a real app, store these securely)
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || randomBytes(32);
const IV = process.env.ENCRYPTION_IV || randomBytes(16);
// Add middleware for encryption
RunCache.use(async (value, context, next) => {
if (context.operation === 'save' && value) {
// Encrypt the data before saving
const cipher = createCipheriv('aes-256-cbc', ENCRYPTION_KEY, IV);
let encrypted = cipher.update(value, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Pass the encrypted data to the next middleware
return next(encrypted);
} else if (context.operation === 'load' && value) {
// Decrypt the data after loading
try {
const decipher = createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, IV);
let decrypted = decipher.update(value, 'hex', 'utf8');
decrypted += decipher.final('utf8');
// Pass the decrypted data to the next middleware
return next(decrypted);
} catch (error) {
console.error('Failed to decrypt cache data:', error);
return next(null);
}
}
return next(value);
});
Compression
For large cache data, you might want to compress it before persisting:
import { gzip, gunzip } from 'zlib';
import { promisify } from 'util';
const gzipAsync = promisify(gzip);
const gunzipAsync = promisify(gunzip);
// Add middleware for compression
RunCache.use(async (value, context, next) => {
if (context.operation === 'save' && value) {
// Compress the data before saving
const compressed = await gzipAsync(Buffer.from(value, 'utf8'));
// Pass the compressed data to the next middleware
return next(compressed.toString('base64'));
} else if (context.operation === 'load' && value) {
// Decompress the data after loading
try {
const decompressed = await gunzipAsync(Buffer.from(value, 'base64'));
// Pass the decompressed data to the next middleware
return next(decompressed.toString('utf8'));
} catch (error) {
console.error('Failed to decompress cache data:', error);
return next(null);
}
}
return next(value);
});
Best Practices
1. Choose the Right Adapter
Select the appropriate storage adapter based on your environment and requirements:
LocalStorageAdapter: For browser environments with small cache data
IndexedDBAdapter: For browser environments with larger cache data
FilesystemAdapter: For Node.js applications
Custom Adapter: For specialized needs (Redis, S3, etc.)
2. Set Appropriate Save Intervals
Balance between data freshness and performance:
// For frequently changing data, save more often
RunCache.setupAutoSave(60000); // 1 minute
// For relatively stable data, save less frequently
RunCache.setupAutoSave(3600000); // 1 hour