Browse Source

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.
Michael Bromley 4 years ago
parent
commit
54dfbf4859

+ 35 - 7
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<V> {
      */
     value(): Promise<V>;
 
+    /**
+     * @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 extends any[], R>(args: Args, fn: (value: V, ...args: Args) => R): Promise<R>;
+
     /**
      * @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<V>(
     const initialValue = await refreshFn();
     let value = initialValue;
     let expires = new Date().getTime() + ttl;
+    const memoCache = new Map<string, any>();
+    const hashArgs = (...args: any[]) => JSON.stringify([args, expires]);
     const refreshValue = (): Promise<V> => {
         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<V>(
                 return value;
             });
     };
+    const getValue = async (): Promise<V> => {
+        const now = new Date().getTime();
+        if (expires < now) {
+            return refreshValue();
+        }
+        return value;
+    };
+    const memoize = async <Args extends any[], R>(
+        args: Args,
+        fn: (value: V, ...args: Args) => R,
+    ): Promise<R> => {
+        const cached = memoCache.get(hashArgs(args));
+        if (cached) {
+            return cached;
+        }
+        let result: Promise<R>;
+        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,
     };
 }

+ 5 - 4
packages/core/src/service/services/zone.service.ts

@@ -40,10 +40,11 @@ export class ZoneService {
     }
 
     async findAll(ctx: RequestContext): Promise<Zone[]> {
-        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;
+            });
         });
     }