Browse Source

feat(core): Create Cache class for a more convenient caching API

Relates to #3043
Michael Bromley 1 year ago
parent
commit
a7ceb74265

+ 59 - 0
packages/core/src/cache/cache.service.ts

@@ -5,6 +5,33 @@ import { ConfigService } from '../config/config.service';
 import { Logger } from '../config/index';
 import { CacheStrategy, SetCacheKeyOptions } from '../config/system/cache-strategy';
 
+import { Cache } from './cache';
+
+/**
+ * @description
+ * Configuration for a new {@link Cache} instance.
+ */
+export interface CacheConfig {
+    /**
+     * @description
+     * A function which generates a cache key from the given id.
+     * This key will be used to store the value in the cache.
+     *
+     * By convention, the key should be namespaced to avoid conflicts.
+     *
+     * @example
+     * ```ts
+     * getKey: id => `MyStrategy.getProductVariantIds.${id}`,
+     * ```
+     */
+    getKey: (id: string | number) => string;
+    /**
+     * @description
+     * Options available when setting the value in the cache.
+     */
+    options?: SetCacheKeyOptions;
+}
+
 /**
  * @description
  * The CacheService is used to cache data in order to optimize performance.
@@ -21,6 +48,38 @@ export class CacheService {
         this.cacheStrategy = this.configService.systemOptions.cacheStrategy;
     }
 
+    /**
+     * @description
+     * Creates a new {@link Cache} instance with the given configuration.
+     *
+     * The `Cache` instance provides a convenience wrapper around the `CacheService`
+     * methods.
+     *
+     * @example
+     * ```ts
+     * const cache = cacheService.createCache({
+     *   getKey: id => `ProductVariantIds.${id}`,
+     *   options: {
+     *     ttl: 1000 * 60 * 60,
+     *     tags: ['products'],
+     *   },
+     * });
+     *
+     * // This will fetch the value from the cache if it exists, or
+     * // fetch it from the ProductService if not, and then cache
+     * // using the key 'ProductVariantIds.${id}'.
+     * const variantIds = await cache.get(id, async () => {
+     *   const variants await ProductService.getVariantsByProductId(ctx, id)
+     *   ;
+     *   // The cached value must be serializable, so we just return the ids
+     *   return variants.map(v => v.id);
+     * });
+     * ```
+     */
+    createCache(config: CacheConfig): Cache {
+        return new Cache(config, this);
+    }
+
     /**
      * @description
      * Gets an item from the cache, or returns undefined if the key is not found, or the

+ 61 - 0
packages/core/src/cache/cache.ts

@@ -0,0 +1,61 @@
+import { JsonCompatible } from '@vendure/common/lib/shared-types';
+
+import { CacheConfig, CacheService } from './cache.service';
+
+/**
+ * @description
+ * A convenience wrapper around the {@link CacheService} methods which provides a simple
+ * API for caching and retrieving data.
+ *
+ * The advantage of using the `Cache` class rather than directly calling the `CacheService`
+ * methods is that it allows you to define a consistent way of generating cache keys and
+ * to set default cache options, and takes care of setting the value in cache if it does not
+ * already exist.
+ *
+ * In most cases, using the `Cache` class will result in simpler and more readable code.
+ *
+ * This class is normally created via the {@link CacheService.createCache} method.
+ */
+export class Cache {
+    constructor(
+        private config: CacheConfig,
+        private cacheService: CacheService,
+    ) {}
+
+    /**
+     * @description
+     * Retrieves the value from the cache if it exists, otherwise calls the `getValueFn` function
+     * to get the value, sets it in the cache and returns it.
+     */
+    async get<T extends JsonCompatible<T>>(
+        id: string | number,
+        getValueFn: () => T | Promise<T>,
+    ): Promise<T> {
+        const key = this.config.getKey(id);
+        const cachedValue = await this.cacheService.get<T>(key);
+        if (cachedValue) {
+            return cachedValue;
+        }
+        const value = await getValueFn();
+        await this.cacheService.set(key, value, this.config.options);
+        return value;
+    }
+
+    /**
+     * @description
+     * Deletes one or more items from the cache.
+     */
+    async delete(id: string | number | Array<string | number>): Promise<void> {
+        const ids = Array.isArray(id) ? id : [id];
+        const keyArgs = ids.map(_id => this.config.getKey(_id));
+        await Promise.all(keyArgs.map(key => this.cacheService.delete(key)));
+    }
+
+    /**
+     * @description
+     * Invalidates one or more tags in the cache.
+     */
+    async invalidateTags(tags: string[]): Promise<void> {
+        await this.cacheService.invalidateTags(tags);
+    }
+}

+ 1 - 0
packages/core/src/cache/index.ts

@@ -1,2 +1,3 @@
 export * from './request-context-cache.service';
 export * from './cache.service';
+export { Cache } from './cache';

+ 14 - 22
packages/core/src/service/helpers/facet-value-checker/facet-value-checker.ts

@@ -63,17 +63,22 @@ export class FacetValueChecker implements OnModuleInit {
      */
     constructor(
         private connection: TransactionalConnection,
-        private cacheService?: CacheService,
+        private cacheService: CacheService,
         private eventBus?: EventBus,
     ) {}
 
+    private facetValueCache = this.cacheService.createCache({
+        getKey: (variantId: ID) => `FacetValueChecker.${variantId}`,
+        options: { ttl: ms('1w') },
+    });
+
     onModuleInit(): any {
         this.eventBus
             ?.ofType(ProductEvent)
             .pipe(filter(event => event.type === 'updated'))
             .subscribe(async event => {
                 if ((event.input as UpdateProductInput)?.facetValueIds) {
-                    const variantIds = await this.connection.rawConnection
+                    const variantIds: ID[] = await this.connection.rawConnection
                         .getRepository(ProductVariant)
                         .createQueryBuilder('variant')
                         .select('variant.id', 'id')
@@ -82,7 +87,7 @@ export class FacetValueChecker implements OnModuleInit {
                         .then(result => result.map(r => r.id));
 
                     if (variantIds.length) {
-                        await this.deleteVariantIdsFromCache(variantIds);
+                        await this.facetValueCache.delete(variantIds);
                     }
                 }
             });
@@ -99,16 +104,12 @@ export class FacetValueChecker implements OnModuleInit {
                         }
                     }
                 }
-                if (updatedVariantIds.length > 0) {
-                    await this.deleteVariantIdsFromCache(updatedVariantIds);
+                if (updatedVariantIds.length) {
+                    await this.facetValueCache.delete(updatedVariantIds);
                 }
             });
     }
 
-    private deleteVariantIdsFromCache(variantIds: ID[]) {
-        return Promise.all(variantIds.map(id => this.cacheService?.delete(this.getCacheKey(id))));
-    }
-
     /**
      * @description
      * Checks a given {@link OrderLine} against the facetValueIds and returns
@@ -117,9 +118,7 @@ export class FacetValueChecker implements OnModuleInit {
      */
     async hasFacetValues(orderLine: OrderLine, facetValueIds: ID[], ctx?: RequestContext): Promise<boolean> {
         const variantId = orderLine.productVariant.id;
-        const cacheKey = this.getCacheKey(variantId);
-        let variantFacetValueIds = await this.cacheService?.get<ID[]>(cacheKey);
-        if (!variantFacetValueIds) {
+        const variantFacetValueIds = await this.facetValueCache.get(variantId, async () => {
             const variant = await this.connection
                 .getRepository(ctx, ProductVariant)
                 .findOne({
@@ -129,21 +128,14 @@ export class FacetValueChecker implements OnModuleInit {
                 })
                 .then(result => result ?? undefined);
             if (!variant) {
-                variantFacetValueIds = [];
+                return [];
             } else {
-                variantFacetValueIds = unique(
-                    [...variant.facetValues, ...variant.product.facetValues].map(fv => fv.id),
-                );
+                return unique([...variant.facetValues, ...variant.product.facetValues].map(fv => fv.id));
             }
-            await this.cacheService?.set(cacheKey, variantFacetValueIds, { ttl: ms('1w') });
-        }
+        });
         return facetValueIds.reduce(
             (result, id) => result && !!(variantFacetValueIds ?? []).find(_id => idsAreEqual(_id, id)),
             true as boolean,
         );
     }
-
-    private getCacheKey(variantId: ID) {
-        return `FacetValueChecker.${variantId}`;
-    }
 }