import { BaseStorage, FindOptions, type Filter } from './base-storage.js'; import { StorageMemory } from './storage-memory.js'; /** * Storage Adapter that takes another Storage Adapter (e.g. IndexedDB) and "syncs" its contents to system memory for faster read access. * * All read operations will use system memory - all write operations will use the provided adapter. */ export class StorageMemorySynced = Record> extends BaseStorage { private isPrimed: boolean; private primePromise: Promise | null; constructor( private inMemoryCache: StorageMemory, private store: BaseStorage, isPrimed = false, ) { super(); this.isPrimed = isPrimed; this.primePromise = null; // Hook into all write operations so that we can sync the In-Memory cache. this.store.on('insert', async (payload) => { await this.inMemoryCache.insertOne(payload.value); this.emit('insert', payload); }); this.store.on('update', async (payload) => { // Remove the old version and insert the new one. We use delete + insert // rather than updateOne because the oldValue is the complete document, // guaranteeing an exact match without assuming any particular key field. await this.inMemoryCache.deleteOne(payload.oldValue); await this.inMemoryCache.insertOne(payload.value); this.emit('update', payload); }); this.store.on('delete', async (payload) => { await this.inMemoryCache.deleteOne(payload.value); // Re-emit the delete event with the original payload. this.emit('delete', payload); }); this.store.on('clear', async () => { // Clear all documents from memory cache await this.inMemoryCache.deleteMany({}); // Re-emit the clear event with the original payload. this.emit('clear', undefined); }); } /** * Ensure the in-memory cache has been initialized from the backing store. * This is especially important for derived children, whose caches start empty. */ private async ensurePrimed(): Promise { if (this.isPrimed) return; if (!this.primePromise) { this.primePromise = (async () => { await this.inMemoryCache.deleteMany({}); const allDocuments = await this.store.find(); await this.inMemoryCache.insertMany(allDocuments); this.isPrimed = true; })(); } await this.primePromise; } static async create>(store: BaseStorage) { // Instantiate in-memory cache and the backing store. const inMemoryCache = new StorageMemory(); // Create instance of this store. const memorySyncedStore = new StorageMemorySynced(inMemoryCache, store); // Sync the data from the backing store into the In-Memory cache. const allDocuments = await store.find(); for (const document of allDocuments) { await inMemoryCache.insertOne(document); } // Return our instance of this store. memorySyncedStore.isPrimed = true; return memorySyncedStore; } async insertMany(documents: Array): Promise { await this.store.insertMany(documents); } async find(filter?: Filter, options?: FindOptions): Promise { await this.ensurePrimed(); return await this.inMemoryCache.find(filter, options); } async updateMany( filter: Filter, update: Partial, options: Partial = {}, ): Promise { await this.ensurePrimed(); return await this.store.updateMany(filter, update, options); } async deleteMany(filter: Filter, options: Partial = {}): Promise { await this.ensurePrimed(); return await this.store.deleteMany(filter, options); } deriveChild>(path: string): BaseStorage { const childStore = this.store.deriveChild(path); const childMemory = this.inMemoryCache.deriveChild(path); if (!(childMemory instanceof StorageMemory)) { throw new Error('Expected derived in-memory cache to be a StorageMemory instance'); } // Create a new synced storage for the child return new StorageMemorySynced(childMemory, childStore); } }