diff --git a/packages/core/src/cache/cache.service.ts b/packages/core/src/cache/cache.service.ts index 3146f7152b..c80e064629 100644 --- a/packages/core/src/cache/cache.service.ts +++ b/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 diff --git a/packages/core/src/cache/cache.ts b/packages/core/src/cache/cache.ts new file mode 100644 index 0000000000..c324e72044 --- /dev/null +++ b/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>( + id: string | number, + getValueFn: () => T | Promise, + ): Promise { + const key = this.config.getKey(id); + const cachedValue = await this.cacheService.get(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): Promise { + 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 { + await this.cacheService.invalidateTags(tags); + } +} diff --git a/packages/core/src/cache/index.ts b/packages/core/src/cache/index.ts index e46af87036..b8aaac3bcd 100644 --- a/packages/core/src/cache/index.ts +++ b/packages/core/src/cache/index.ts @@ -1,2 +1,3 @@ export * from './request-context-cache.service'; export * from './cache.service'; +export { Cache } from './cache'; diff --git a/packages/core/src/service/helpers/facet-value-checker/facet-value-checker.ts b/packages/core/src/service/helpers/facet-value-checker/facet-value-checker.ts index 653d55c969..31fb11f24a 100644 --- a/packages/core/src/service/helpers/facet-value-checker/facet-value-checker.ts +++ b/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 { const variantId = orderLine.productVariant.id; - const cacheKey = this.getCacheKey(variantId); - let variantFacetValueIds = await this.cacheService?.get(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}`; - } }