Maison > interface Web > js tutoriel > Amélioration du cache LRU avec une persistance des données configurable

Amélioration du cache LRU avec une persistance des données configurable

Barbara Streisand
Libérer: 2024-12-26 17:38:10
original
644 Les gens l'ont consulté

Enhancing LRU Cache with Configurable Data Persistence

En nous appuyant sur les bases de ce guide sur la création d'un cache en mémoire, nous irons plus loin en introduisant la persistance des données configurable. En tirant parti des modèles Adaptateur et Stratégie, nous concevrons un système extensible qui dissocie les mécanismes de stockage de la logique de mise en cache, permettant une intégration transparente des bases de données ou des services selon les besoins.

La vision : un découplage comme un ORM

L'objectif est de rendre le cache extensible sans altérer sa logique de base. Inspirée des systèmes ORM, notre approche implique une abstraction API partagée. Cela permet au stockage, tel que localStorage, IndexedDB ou même une base de données distante, de fonctionner de manière interchangeable avec un minimum de modifications de code.

La classe de base de l'adaptateur de stockage

Voici la classe abstraite définissant l'API pour tout système de persistance :

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>;
}
Copier après la connexion

Toute solution de stockage doit étendre cette classe de base, garantissant la cohérence des interactions. Par exemple, voici l’implémentation pour IndexedDB :

Exemple : adaptateur IndexedDB

Cet adaptateur implémente l'interface StorageAdapter pour conserver les données du cache dans un magasin IndexedDB.

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);
    });
  }
}
Copier après la connexion

Intégration de l'adaptateur dans le cache

Le cache accepte un StorageAdapter en option. S'il est fourni, il initialise la connexion à la base de données, charge les données en mémoire et maintient le cache et le stockage synchronisés.

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;
}
Copier après la connexion

Pourquoi des modèles d’adaptateur et de stratégie ?

Utilisation du modèle d'adaptateur :

  • Dissocie le cache des mécanismes de stockage spécifiques.
  • Garantit l'extensibilité pour les nouveaux backends de stockage.

Combinaison avec le modèle Stratégie :

  • Active la sélection à l'exécution de la couche de persistance.
  • Simplifie les tests en se moquant de différents adaptateurs.

Pratiques de conception clés

  • API abstraite : Maintient la logique du cache indépendamment des détails de stockage.
  • Singleton Cache : Assure la cohérence de l'état partagé.
  • Initialisation asynchrone :Évite de bloquer les opérations lors de la configuration.
  • Lazy Loading : Charge uniquement les données persistantes lorsqu'un adaptateur de stockage est fourni.

Prochaines étapes

Cette conception est robuste mais laisse place à des améliorations :

  • Optimisez la logique de synchronisation pour de meilleures performances.
  • Expérimentez avec des adaptateurs supplémentaires comme Redis ou SQLite.

Essayez-le ! ?

Si vous souhaitez tester le cache en action, il est disponible sous forme de package npm : adev-lru. Vous pouvez également explorer le code source complet sur GitHub : référentiel adev-lru. J'apprécie toutes les recommandations, commentaires constructifs ou contributions pour le rendre encore meilleur ! ?

Bon codage ! ?

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal