Skip to content

Commit

Permalink
feat(core): Create Cache class for a more convenient caching API
Browse files Browse the repository at this point in the history
Relates to #3043
  • Loading branch information
michaelbromley committed Oct 30, 2024
1 parent 9d99593 commit a7ceb74
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 22 deletions.
59 changes: 59 additions & 0 deletions packages/core/src/cache/cache.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
61 changes: 61 additions & 0 deletions packages/core/src/cache/cache.ts
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions packages/core/src/cache/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './request-context-cache.service';
export * from './cache.service';
export { Cache } from './cache';
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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);
}
}
});
Expand All @@ -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
Expand All @@ -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({
Expand All @@ -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}`;
}
}

0 comments on commit a7ceb74

Please sign in to comment.