From 54dfbf4859f7f347ab2b0a8c0245f9985745297c Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Tue, 28 Sep 2021 09:27:28 +0200 Subject: [PATCH] perf(core): Use memoization when caching zone members Relates to #988. Zone members (countries) were being newly translated for _every_ call to ZoneService.findAll(). In practice this was resulting in thousands of calls to `translateDeep`. This optimization cuts the calls down by a factor of 5-10x in various scenarios. --- .../core/src/common/self-refreshing-cache.ts | 42 +++++++++++++++---- .../core/src/service/services/zone.service.ts | 9 ++-- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/packages/core/src/common/self-refreshing-cache.ts b/packages/core/src/common/self-refreshing-cache.ts index 069a3ba7eb..8fa74cf231 100644 --- a/packages/core/src/common/self-refreshing-cache.ts +++ b/packages/core/src/common/self-refreshing-cache.ts @@ -1,3 +1,5 @@ +import { Json } from '@vendure/common/lib/shared-types'; + import { Logger } from '../config/logger/vendure-logger'; /** @@ -12,6 +14,15 @@ export interface SelfRefreshingCache { */ value(): Promise; + /** + * @description + * Allows a memoized function to be defined. For the given arguments, the `fn` function will + * be invoked only once and its output cached and returned. + * The results cache is cleared along with the rest of the cache according to the configured + * `ttl` value. + */ + memoize(args: Args, fn: (value: V, ...args: Args) => R): Promise; + /** * @description * Force a refresh of the value, e.g. when it is known that the value has changed such as after @@ -43,12 +54,15 @@ export async function createSelfRefreshingCache( const initialValue = await refreshFn(); let value = initialValue; let expires = new Date().getTime() + ttl; + const memoCache = new Map(); + const hashArgs = (...args: any[]) => JSON.stringify([args, expires]); const refreshValue = (): Promise => { Logger.debug(`Refreshing the SelfRefreshingCache "${name}"`); return refreshFn() .then(newValue => { value = newValue; expires = new Date().getTime() + ttl; + memoCache.clear(); return value; }) .catch(err => { @@ -60,14 +74,28 @@ export async function createSelfRefreshingCache( return value; }); }; + const getValue = async (): Promise => { + const now = new Date().getTime(); + if (expires < now) { + return refreshValue(); + } + return value; + }; + const memoize = async ( + args: Args, + fn: (value: V, ...args: Args) => R, + ): Promise => { + const cached = memoCache.get(hashArgs(args)); + if (cached) { + return cached; + } + let result: Promise; + memoCache.set(hashArgs(args), (result = getValue().then(val => fn(val, ...args)))); + return result; + }; return { - async value() { - const now = new Date().getTime(); - if (expires < now) { - return refreshValue(); - } - return value; - }, + value: getValue, refresh: refreshValue, + memoize, }; } diff --git a/packages/core/src/service/services/zone.service.ts b/packages/core/src/service/services/zone.service.ts index 428bd517ba..66ccc25aea 100644 --- a/packages/core/src/service/services/zone.service.ts +++ b/packages/core/src/service/services/zone.service.ts @@ -40,10 +40,11 @@ export class ZoneService { } async findAll(ctx: RequestContext): Promise { - const zones = await this.zones.value(); - return zones.map(zone => { - zone.members = zone.members.map(country => translateDeep(country, ctx.languageCode)); - return zone; + return this.zones.memoize([ctx.languageCode], (zones, languageCode) => { + return zones.map(zone => { + zone.members = zone.members.map(country => translateDeep(country, ctx.languageCode)); + return zone; + }); }); }