ホームページ >ウェブフロントエンド >jsチュートリアル >構成可能なデータ永続性による LRU キャッシュの強化

構成可能なデータ永続性による LRU キャッシュの強化

Barbara Streisand
Barbara Streisandオリジナル
2024-12-26 17:38:10637ブラウズ

Enhancing LRU Cache with Configurable Data Persistence

メモリ内キャッシュの作成に関するこのガイドの基礎に基づいて、構成可能なデータの永続性を導入してさらに進めていきます。アダプター パターンとストラテジー パターンを活用することで、ストレージ メカニズムをキャッシュ ロジックから切り離し、必要に応じてデータベースやサービスをシームレスに統合できる拡張可能なシステムを設計します。

ビジョン: ORM のようにデカップリングする

目標は、コアロジックを変更せずにキャッシュを拡張可能にすることです。 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 アダプター

このアダプターは、キャッシュ データを 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;
}

アダプターと戦略パターンを使用する理由

アダプター パターンの使用:

  • キャッシュを特定のストレージ メカニズムから分離します。
  • 新しいストレージ バックエンドの拡張性を確保します。

戦略パターンとの組み合わせ:

  • 永続化レイヤーのランタイム選択を有効にします。
  • さまざまなアダプターをモックすることでテストを簡素化します。

主要な設計実践

  • 抽象 API: キャッシュ ロジックをストレージの詳細にとらわれないようにします。
  • シングルトン キャッシュ: 共有状態の一貫性を確保します。
  • 非同期初期化: セットアップ中の操作のブロックを回避します。
  • 遅延読み込み: ストレージ アダプターが提供されている場合、永続化されたデータのみを読み込みます。

次のステップ

この設計は堅牢ですが、拡張の余地があります:

  • パフォーマンスを向上させるために同期ロジックを最適化します。
  • Redis や SQLite などの追加アダプターを試してください。

試してみてください! ?

動作中のキャッシュをテストしたい場合は、npm パッケージ adev-lru として利用できます。 GitHub: adev-lru リポジトリで完全なソース コードを探索することもできます。より良いものにするための提案、建設的なフィードバック、または貢献を歓迎します。 ?

コーディングを楽しんでください! ?

以上が構成可能なデータ永続性による LRU キャッシュの強化の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。