ホームページ >ウェブフロントエンド >jsチュートリアル >構成可能なデータ永続性による LRU キャッシュの強化
メモリ内キャッシュの作成に関するこのガイドの基礎に基づいて、構成可能なデータの永続性を導入してさらに進めていきます。アダプター パターンとストラテジー パターンを活用することで、ストレージ メカニズムをキャッシュ ロジックから切り離し、必要に応じてデータベースやサービスをシームレスに統合できる拡張可能なシステムを設計します。
目標は、コアロジックを変更せずにキャッシュを拡張可能にすることです。 ORM システムからインスピレーションを得た私たちのアプローチには、共有 API 抽象化が含まれています。これにより、localStorage、IndexedDB、さらにはリモート データベースなどのストレージが、最小限のコード変更で互換的に動作できるようになります。
永続化システムの API を定義する抽象クラスは次のとおりです。
export abstract class StorageAdapter { abstract connect(): Promise<void>; abstract add(key: string, value: unknown): Promise<void>; abstract get(key: string): Promise<unknown | null>; abstract getAll(): Promise<Record<string, unknown>>; abstract delete(key: string): Promise<void>; abstract clear(): Promise<void>; }
すべてのストレージ ソリューションは、この基本クラスを拡張して、対話の一貫性を確保する必要があります。たとえば、IndexedDB の実装は次のとおりです:
このアダプターは、キャッシュ データを IndexedDB ストアに保持するための StorageAdapter インターフェイスを実装します。
import { StorageAdapter } from './storage_adapter'; /** * IndexedDBAdapter is an implementation of the StorageAdapter * interface designed to provide a persistent storage mechanism * using IndexedDB. This adapter can be reused for other cache * implementations or extended for similar use cases, ensuring * flexibility and scalability. */ export class IndexedDBAdapter extends StorageAdapter { private readonly dbName: string; private readonly storeName: string; private db: IDBDatabase | null = null; /** * Initializes the adapter with the specified database and store * names. Defaults are provided to make it easy to set up without * additional configuration. */ constructor(dbName: string = 'cacheDB', storeName: string = 'cacheStore') { super(); this.dbName = dbName; this.storeName = storeName; } /** * Connects to the IndexedDB database and initializes it if * necessary. This asynchronous method ensures that the database * and object store are available before any other operations. * It uses the `onupgradeneeded` event to handle schema creation * or updates, making it a robust solution for versioning. */ async connect(): Promise<void> { return await new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, 1); request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains(this.storeName)) { db.createObjectStore(this.storeName, { keyPath: 'key' }); } }; request.onsuccess = (event) => { this.db = (event.target as IDBOpenDBRequest).result; resolve(); }; request.onerror = () => reject(request.error); }); } /** * Adds or updates a key-value pair in the store. This method is * asynchronous to ensure compatibility with the non-blocking * nature of IndexedDB and to prevent UI thread blocking. Using * the `put` method ensures idempotency: the operation will * insert or replace the entry. */ async add(key: string, value: unknown): Promise<void> { await this._withTransaction('readwrite', (store) => store.put({ key, value })); } /** * Retrieves the value associated with a key. If the key does not * exist, null is returned. This method is designed to integrate * seamlessly with caching mechanisms, enabling fast lookups. */ async get(key: string): Promise<unknown | null> { return await this._withTransaction('readonly', (store) => this._promisifyRequest(store.get(key)).then((result) => result ? (result as { key: string; value: unknown }).value : null ) ); } /** * Fetches all key-value pairs from the store. Returns an object * mapping keys to their values, making it suitable for bulk * operations or syncing with in-memory caches. */ async getAll(): Promise<Record<string, unknown>> { return await this._withTransaction('readonly', (store) => this._promisifyRequest(store.getAll()).then((results) => results.reduce((acc: Record<string, unknown>, item: { key: string; value: unknown }) => { acc[item.key] = item.value; return acc; }, {}) ) ); } /** * Deletes a key-value pair by its key. This method is crucial * for managing cache size and removing expired entries. The * `readwrite` mode is used to ensure proper deletion. */ async delete(key: string): Promise<void> { await this._withTransaction('readwrite', (store) => store.delete(key)); } /** * Clears all entries from the store. This method is ideal for * scenarios where the entire cache needs to be invalidated, such * as during application updates or environment resets. */ async clear(): Promise<void> { await this._withTransaction('readwrite', (store) => store.clear()); } /** * Handles transactions in a reusable way. Ensures the database * is connected and abstracts the transaction logic. By * centralizing transaction handling, this method reduces * boilerplate code and ensures consistency across all operations. */ private async _withTransaction<T>( mode: IDBTransactionMode, callback: (store: IDBObjectStore) => IDBRequest | Promise<T> ): Promise<T> { if (!this.db) throw new Error('IndexedDB is not connected'); const transaction = this.db.transaction([this.storeName], mode); const store = transaction.objectStore(this.storeName); const result = callback(store); return result instanceof IDBRequest ? await this._promisifyRequest(result) : await result; } /** * Converts IndexedDB request events into Promises, allowing for * cleaner and more modern asynchronous handling. This is * essential for making IndexedDB operations fit seamlessly into * the Promise-based architecture of JavaScript applications. */ private async _promisifyRequest<T>(request: IDBRequest): Promise<T> { return await new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result as T); request.onerror = () => reject(request.error); }); } }
キャッシュはオプションの StorageAdapter を受け入れます。指定すると、データベース接続が初期化され、データがメモリにロードされ、キャッシュとストレージの同期が維持されます。
private constructor(capacity: number, storageAdapter?: StorageAdapter) { this.capacity = capacity; this.storageAdapter = storageAdapter; if (this.storageAdapter) { this.storageAdapter.connect().catch((error) => { throw new Error(error); }); this.storageAdapter.getAll().then((data) => { for (const key in data) { this.put(key, data[key] as T); } }).catch((error) => { throw new Error(error); }); } this.hash = new Map(); this.head = this.tail = undefined; this.hitCount = this.missCount = this.evictionCount = 0; }
アダプター パターンの使用:
戦略パターンとの組み合わせ:
この設計は堅牢ですが、拡張の余地があります:
動作中のキャッシュをテストしたい場合は、npm パッケージ adev-lru として利用できます。 GitHub: adev-lru リポジトリで完全なソース コードを探索することもできます。より良いものにするための提案、建設的なフィードバック、または貢献を歓迎します。 ?
コーディングを楽しんでください! ?
以上が構成可能なデータ永続性による LRU キャッシュの強化の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。