How the Prompt-Optimizer Storage Layer Handles Multiple Providers: Dexie, LocalStorage, and File
The prompt-optimizer storage layer abstracts all persistent data behind the IStorageProvider interface and uses a StorageFactory to instantiate Dexie (IndexedDB), LocalStorage, or File-based providers depending on the runtime environment.
The linshenkx/prompt-optimizer project implements a unified storage architecture that seamlessly adapts to web browsers, Electron desktops, and test environments. By decoupling the business logic from specific storage mechanisms, the prompt-optimizer storage layer ensures consistent data persistence across platforms while leveraging the unique capabilities of each backend.
The Storage Abstraction Interface
All storage implementations conform to the IStorageProvider interface defined in packages/core/src/services/storage/types.ts. This contract standardizes CRUD operations, transactional capabilities, and provider metadata across every backend.
The StorageFactory in packages/core/src/services/storage/factory.ts manages provider instantiation and caching:
// packages/core/src/services/storage/factory.ts
static create(type: StorageType): IStorageProvider {
// Singleton pattern: reuse existing instances
if (this.instances.has(type)) {
return this.instances.get(type)!;
}
let instance: IStorageProvider;
switch (type) {
case 'localStorage':
instance = new LocalStorageProvider();
break;
case 'dexie':
instance = new DexieStorageProvider();
break;
case 'file':
// Electron-specific, instantiated directly in main process
break;
}
this.instances.set(type, instance);
return instance;
}
The factory exposes isSupported(type) to verify runtime availability (e.g., checking for IndexedDB) and getSupportedTypes() to list viable providers for the current platform.
Dexie Storage Provider (IndexedDB)
The DexieStorageProvider in packages/core/src/services/storage/dexieStorageProvider.ts delivers high-performance, transactional storage using IndexedDB via the Dexie.js wrapper.
Database Schema:
- Name:
PromptOptimizerDB(configurable viawindow.__TEST_DB_NAME__for testing) - Table:
storagewith schema++id, key, value, timestamp
Core API Implementation:
// packages/core/src/services/storage/dexieStorageProvider.ts
async setItem(key: string, value: any): Promise<void> {
await this.db.storage.put({
key,
value,
timestamp: Date.now()
});
}
async getItem(key: string): Promise<any> {
const record = await this.db.storage.get(key);
return record?.value ?? null;
}
async removeItem(key: string): Promise<void> {
await this.db.storage.delete(key);
}
Advanced Transactional Features:
atomicUpdate– Implements optimistic concurrency with exponential backoff for conflicting writesbatchUpdate– Bulk operations usingbulkPutandbulkDeletewithin a single transactionexportAll / importAll– Full database serialization for backup and migration
Capabilities: supportsAtomic: true, supportsBatch: true, no fixed size limit.
LocalStorage Provider
The LocalStorageProvider in packages/core/src/services/storage/localStorageProvider.ts wraps the browser's synchronous localStorage API with asynchronous locking to prevent race conditions.
Concurrency Control:
Each key operates under an independent async lock (async-mutex), ensuring that concurrent read-modify-write cycles serialize correctly:
// packages/core/src/services/storage/localStorageProvider.ts
async updateData(key: string, modifier: (data: any) => any): Promise<void> {
const release = await this.lock.acquire(key);
try {
const current = localStorage.getItem(key);
const parsed = current ? JSON.parse(current) : null;
const newValue = modifier(parsed);
localStorage.setItem(key, JSON.stringify(newValue));
} finally {
release();
}
}
Standard API: All methods return Promises to maintain interface consistency with other providers:
getItem(key)– Retrieves and parses JSONsetItem(key, value)– Serializes and storesremoveItem(key)– Deletes entryclearAll()– Clears entire localStorage
Capabilities: supportsAtomic: true (via locking), supportsBatch: true, approximately 5 MiB maximum storage.
File Storage Provider (Electron)
The FileStorageProvider in packages/core/src/services/storage/fileStorageProvider.ts implements durable persistence for Electron applications using the local filesystem.
Storage Location:
Persists to prompt-optimizer-data.json within Electron's userData directory.
Robust Initialization: The provider implements a recovery-aware initialization sequence that attempts to load the primary file, falls back to a backup copy, and initializes a fresh store if both fail:
// packages/core/src/services/storage/fileStorageProvider.ts
async initialize(): Promise<void> {
await this.loadFromFileWithRecovery();
}
private async loadFromFileWithRecovery(): Promise<void> {
// Attempt primary file, then backup, then fresh start
// Implementation handles corrupted JSON gracefully
}
Atomic Write Strategy: To prevent data corruption during crashes, writes use atomic file operations:
- Serialize data to a temporary file
- Rename temporary file to target (atomic on POSIX/Windows)
Debounced Persistence:
Writes are debounced (WRITE_DELAY = 500ms) to batch rapid successive updates. The flush() method forces immediate synchronization with configurable timeout and retry limits:
// packages/core/src/services/storage/fileStorageProvider.ts
async setItem(key: string, value: any): Promise<void> {
this.data.set(key, value);
this.scheduleWrite(); // Debounced flush
}
async flush(): Promise<void> {
// Immediate write with retry logic
await this.atomicWrite(JSON.stringify(Object.fromEntries(this.data)));
}
Capabilities: supportsAtomic: true, supportsBatch: true, unlimited storage (disk-constrained).
Runtime Provider Selection
The application selects the appropriate provider based on the execution context:
import { StorageFactory } from '@/services/storage/factory';
const storage = StorageFactory.create(
typeof window === 'undefined'
? 'file' // Electron main process (Node.js)
: window.indexedDB
? 'dexie' // Modern browser with IndexedDB
: 'localStorage' // Fallback for legacy browsers
);
This detection logic ensures optimal storage selection: Dexie for high-performance web applications, LocalStorage for simple browser fallback, and File for Electron desktop builds requiring unlimited persistence.
Summary
- The prompt-optimizer storage layer uses the
IStorageProviderinterface to abstract all persistence operations, enabling seamless swapping between backends without changing business logic. - StorageFactory (
packages/core/src/services/storage/factory.ts) implements a singleton pattern that caches provider instances and exposes runtime capability detection viaisSupported()andgetSupportedTypes(). - DexieStorageProvider leverages IndexedDB with transactional support for
atomicUpdate,batchUpdate, and full databaseexportAll/importAlloperations. - LocalStorageProvider wraps the synchronous browser API with per-key async locks to provide atomic read-modify-write cycles despite localStorage's single-threaded nature.
- FileStorageProvider delivers Electron-specific persistence with recovery-aware initialization, debounced atomic file writes, and unlimited storage capacity.
Frequently Asked Questions
How does the storage factory decide which provider to instantiate?
The StorageFactory.create() method accepts a StorageType string ('dexie', 'localStorage', or 'file') and returns the corresponding singleton instance. Runtime selection logic typically checks typeof window to detect Electron's main process (selecting 'file'), then falls back to checking window.indexedDB availability for 'dexie', otherwise defaulting to 'localStorage'.
What happens if the Electron file storage becomes corrupted?
The FileStorageProvider implements a recovery chain in loadFromFileWithRecovery(). It first attempts to parse the primary prompt-optimizer-data.json. If that fails, it attempts to load from a backup file. If both fail, it initializes a fresh empty data store, ensuring the application remains functional even after filesystem corruption.
Can I use atomic updates with LocalStorage despite it being synchronous?
Yes. While localStorage itself is synchronous, the LocalStorageProvider wraps all operations with per-key async locks using the async-mutex library. This ensures that updateData calls serialize concurrent modifications, providing atomic read-modify-write semantics even when multiple async operations attempt to modify the same key simultaneously.
What are the storage size limits for each provider?
DexieStorageProvider has no fixed size limit beyond the browser's IndexedDB quota (typically 50MB+ depending on the browser). LocalStorageProvider is constrained to approximately 5 MiB total for all keys combined due to browser limitations. FileStorageProvider has no practical limit beyond available disk space, making it suitable for large datasets in Electron applications.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →