Skip to content

Commit

Permalink
feat(core): Implement CacheStrategy and CacheService
Browse files Browse the repository at this point in the history
Relates to #3043
  • Loading branch information
michaelbromley committed Sep 10, 2024
1 parent 8f22ef8 commit 489c9c0
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 2 deletions.
8 changes: 6 additions & 2 deletions packages/core/src/cache/cache.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Module } from '@nestjs/common';

import { ConfigModule } from '../config/config.module';

import { CacheService } from './cache.service';
import { RequestContextCacheService } from './request-context-cache.service';

@Module({
providers: [RequestContextCacheService],
exports: [RequestContextCacheService],
imports: [ConfigModule],
providers: [RequestContextCacheService, CacheService],
exports: [RequestContextCacheService, CacheService],
})
export class CacheModule {}
74 changes: 74 additions & 0 deletions packages/core/src/cache/cache.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Injectable } from '@nestjs/common';
import { JsonCompatible } from '@vendure/common/lib/shared-types';

import { ConfigService } from '../config/config.service';
import { Logger } from '../config/index';
import { CacheStrategy, SetCacheKeyOptions } from '../config/system/cache-strategy';

/**
* @description
* The CacheService is used to cache data in order to optimize performance.
*
* Internally it makes use of the configured {@link CacheStrategy} to persist
* the cache into a key-value store.
*
* @since 3.1.0
*/
@Injectable()
export class CacheService {
protected cacheStrategy: CacheStrategy;
constructor(private configService: ConfigService) {
this.cacheStrategy = this.configService.systemOptions.cacheStrategy;
}

/**
* @description
* Gets an item from the cache, or returns undefined if the key is not found, or the
* item has expired.
*/
async get<T extends JsonCompatible<T>>(key: string): Promise<T | undefined> {
try {
const result = await this.cacheStrategy.get(key);
if (result) {
Logger.debug(`CacheService hit for key [${key}]`);
}
return result as T;
} catch (e: any) {
Logger.error(`Could not get key [${key}] from CacheService`, undefined, e.stack);
}
}

/**
* @description
* Sets a key-value pair in the cache. The value must be serializable, so cannot contain
* things like functions, circular data structures, class instances etc.
*
* Optionally a "time to live" (ttl) can be specified, which means that the key will
* be considered stale after that many milliseconds.
*/
async set<T extends JsonCompatible<T>>(
key: string,
value: T,
options?: SetCacheKeyOptions,
): Promise<void> {
try {
await this.cacheStrategy.set(key, value, options);
Logger.debug(`Set key [${key}] in CacheService`);
} catch (e: any) {
Logger.error(`Could not set key [${key}] in CacheService`, undefined, e.stack);
}
}

/**
* @description
* Deletes an item from the cache.
*/
async delete(key: string): Promise<void> {
try {
await this.cacheStrategy.delete(key);
Logger.debug(`Deleted key [${key}] from CacheService`);
} catch (e: any) {
Logger.error(`Could not delete key [${key}] from CacheService`, undefined, e.stack);
}
}
}
2 changes: 2 additions & 0 deletions packages/core/src/config/default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { InMemorySessionCacheStrategy } from './session-cache/in-memory-session-
import { defaultShippingCalculator } from './shipping-method/default-shipping-calculator';
import { defaultShippingEligibilityChecker } from './shipping-method/default-shipping-eligibility-checker';
import { DefaultShippingLineAssignmentStrategy } from './shipping-method/default-shipping-line-assignment-strategy';
import { InMemoryCacheStrategy } from './system/in-memory-cache-strategy';
import { DefaultTaxLineCalculationStrategy } from './tax/default-tax-line-calculation-strategy';
import { DefaultTaxZoneStrategy } from './tax/default-tax-zone-strategy';
import { RuntimeVendureConfig } from './vendure-config';
Expand Down Expand Up @@ -220,6 +221,7 @@ export const defaultConfig: RuntimeVendureConfig = {
},
plugins: [],
systemOptions: {
cacheStrategy: new InMemoryCacheStrategy({ cacheSize: 10_000 }),
healthChecks: [new TypeORMHealthCheckStrategy()],
errorHandlers: [],
},
Expand Down
52 changes: 52 additions & 0 deletions packages/core/src/config/system/cache-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { JsonCompatible } from '@vendure/common/lib/shared-types';

import { InjectableStrategy } from '../../common/types/injectable-strategy';

/**
* @description
* Options available when setting the value in the cache.
*/
export interface SetCacheKeyOptions {
/**
* @description
* The time-to-live for the cache key in milliseconds. This means
* that after this time period, the key will be considered stale
* and will no longer be returned from the cache. Omitting
* this is equivalent to having an infinite ttl.
*/
ttl?: number;
}

/**
* @description
* The CacheStrategy defines how the underlying shared cache mechanism is implemented.
*
* It is used by the {@link CacheService} to take care of storage and retrieval of items
* from the cache.
*
* @since 3.1.0
*/
export interface CacheStrategy extends InjectableStrategy {
/**
* @description
* Gets an item from the cache, or returns undefined if the key is not found, or the
* item has expired.
*/
get<T extends JsonCompatible<T>>(key: string): Promise<T | undefined>;

/**
* @description
* Sets a key-value pair in the cache. The value must be serializable, so cannot contain
* things like functions, circular data structures, class instances etc.
*
* Optionally a "time to live" (ttl) can be specified, which means that the key will
* be considered stale after that many milliseconds.
*/
set<T extends JsonCompatible<T>>(key: string, value: T, options?: SetCacheKeyOptions): Promise<void>;

/**
* @description
* Deletes an item from the cache.
*/
delete(key: string): Promise<void>;
}
63 changes: 63 additions & 0 deletions packages/core/src/config/system/in-memory-cache-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { JsonCompatible } from '@vendure/common/lib/shared-types';

import { CacheStrategy, SetCacheKeyOptions } from './cache-strategy';

export interface CacheItem<T> {
value: JsonCompatible<T>;
expires?: number;
}

/**
* A {@link CacheStrategy} that stores the cache in memory using a simple
* JavaScript Map.
*
* **Caution** do not use this in a multi-instance deployment because
* cache invalidation will not propagate to other instances.
*
* @since 3.1.0
*/
export class InMemoryCacheStrategy implements CacheStrategy {
protected cache = new Map<string, CacheItem<any>>();
protected cacheSize = 10_000;

constructor(config?: { cacheSize?: number }) {
if (config?.cacheSize) {
this.cacheSize = config.cacheSize;
}
}

async get<T extends JsonCompatible<T>>(key: string): Promise<T | undefined> {
const hit = this.cache.get(key);
if (hit) {
const now = new Date().getTime();
if (!hit.expires || (hit.expires && now < hit.expires)) {
return hit.value;
} else {
this.cache.delete(key);
}
}
}

async set<T extends JsonCompatible<T>>(key: string, value: T, options?: SetCacheKeyOptions) {
if (this.cache.has(key)) {
// delete key to put the item to the end of
// the cache, marking it as new again
this.cache.delete(key);
} else if (this.cache.size === this.cacheSize) {
// evict oldest
this.cache.delete(this.first());
}
this.cache.set(key, {
value,
expires: options?.ttl ? new Date().getTime() + options.ttl : undefined,
});
}

async delete(key: string) {
this.cache.delete(key);
}

private first() {
return this.cache.keys().next().value;
}
}
10 changes: 10 additions & 0 deletions packages/core/src/config/vendure-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { SessionCacheStrategy } from './session-cache/session-cache-strategy';
import { ShippingCalculator } from './shipping-method/shipping-calculator';
import { ShippingEligibilityChecker } from './shipping-method/shipping-eligibility-checker';
import { ShippingLineAssignmentStrategy } from './shipping-method/shipping-line-assignment-strategy';
import { CacheStrategy } from './system/cache-strategy';
import { ErrorHandlerStrategy } from './system/error-handler-strategy';
import { HealthCheckStrategy } from './system/health-check-strategy';
import { TaxLineCalculationStrategy } from './tax/tax-line-calculation-strategy';
Expand Down Expand Up @@ -1058,6 +1059,15 @@ export interface SystemOptions {
* @since 2.2.0
*/
errorHandlers?: ErrorHandlerStrategy[];
/**
* @description
* Defines the underlying method used to store cache key-value pairs which powers the
* {@link CacheService}.
*
* @since 3.1.0
* @default InMemoryCacheStrategy
*/
cacheStrategy?: CacheStrategy;
}

/**
Expand Down

0 comments on commit 489c9c0

Please sign in to comment.