self-refreshing-cache.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import { Json } from '@vendure/common/lib/shared-types';
  2. import { Logger } from '../config/logger/vendure-logger';
  3. /**
  4. * @description
  5. * A cache which automatically refreshes itself if the value is found to be stale.
  6. */
  7. export interface SelfRefreshingCache<V, RefreshArgs extends any[] = []> {
  8. /**
  9. * @description
  10. * The current value of the cache. If the value is stale, the data will be refreshed and then
  11. * the fresh value will be returned.
  12. */
  13. value(): Promise<V>;
  14. value(...refreshArgs: RefreshArgs | [undefined]): Promise<V>;
  15. /**
  16. * @description
  17. * Allows a memoized function to be defined. For the given arguments, the `fn` function will
  18. * be invoked only once and its output cached and returned.
  19. * The results cache is cleared along with the rest of the cache according to the configured
  20. * `ttl` value.
  21. */
  22. memoize<Args extends any[], R>(
  23. args: Args,
  24. refreshArgs: RefreshArgs,
  25. fn: (value: V, ...args: Args) => R,
  26. ): Promise<R>;
  27. /**
  28. * @description
  29. * Force a refresh of the value, e.g. when it is known that the value has changed such as after
  30. * an update operation to the source data in the database.
  31. */
  32. refresh(...args: RefreshArgs): Promise<V>;
  33. }
  34. export interface SelfRefreshingCacheConfig<V, RefreshArgs extends any[]> {
  35. name: string;
  36. ttl: number;
  37. refresh: {
  38. fn: (...args: RefreshArgs) => Promise<V>;
  39. /**
  40. * Default arguments, passed to refresh function
  41. */
  42. defaultArgs: RefreshArgs;
  43. };
  44. /**
  45. * Intended for unit testing the SelfRefreshingCache only.
  46. * By default uses `() => new Date().getTime()`
  47. */
  48. getTimeFn?: () => number;
  49. }
  50. /**
  51. * @description
  52. * Creates a {@link SelfRefreshingCache} object, which is used to cache a single frequently-accessed value. In this type
  53. * of cache, the function used to populate the value (`refreshFn`) is defined during the creation of the cache, and
  54. * it is immediately used to populate the initial value.
  55. *
  56. * From there, when the `.value` property is accessed, it will return a value from the cache, and if the
  57. * value has expired, it will automatically run the `refreshFn` to update the value and then return the
  58. * fresh value.
  59. */
  60. export async function createSelfRefreshingCache<V, RefreshArgs extends any[]>(
  61. config: SelfRefreshingCacheConfig<V, RefreshArgs>,
  62. refreshArgs?: RefreshArgs
  63. ): Promise<SelfRefreshingCache<V, RefreshArgs>> {
  64. const { ttl, name, refresh, getTimeFn } = config;
  65. const getTimeNow = getTimeFn ?? (() => new Date().getTime());
  66. const initialValue = await refresh.fn(...(refreshArgs ?? refresh.defaultArgs));
  67. let value = initialValue;
  68. let expires = getTimeNow() + ttl;
  69. const memoCache = new Map<string, { expires: number; value: any }>();
  70. const refreshValue = (resetMemoCache = true, args: RefreshArgs): Promise<V> => {
  71. return refresh
  72. .fn(...args)
  73. .then(newValue => {
  74. value = newValue;
  75. expires = getTimeNow() + ttl;
  76. if (resetMemoCache) {
  77. memoCache.clear();
  78. }
  79. return value;
  80. })
  81. .catch(err => {
  82. Logger.error(
  83. `Failed to update SelfRefreshingCache "${name}": ${err.message}`,
  84. undefined,
  85. err.stack,
  86. );
  87. return value;
  88. });
  89. };
  90. const getValue = async (refreshArgs?: RefreshArgs, resetMemoCache = true): Promise<V> => {
  91. const now = getTimeNow();
  92. if (expires < now) {
  93. return refreshValue(resetMemoCache, refreshArgs ?? refresh.defaultArgs);
  94. }
  95. return value;
  96. };
  97. const memoize = async <Args extends any[], R>(
  98. args: Args,
  99. refreshArgs: RefreshArgs,
  100. fn: (value: V, ...args: Args) => R,
  101. ): Promise<R> => {
  102. const key = JSON.stringify(args);
  103. const cached = memoCache.get(key);
  104. const now = getTimeNow();
  105. if (cached && now < cached.expires) {
  106. return cached.value;
  107. }
  108. const result = getValue(refreshArgs, false).then(val => fn(val, ...args));
  109. memoCache.set(key, {
  110. expires: now + ttl,
  111. value: result,
  112. });
  113. return result;
  114. };
  115. return {
  116. value: (...args) => getValue(!args.length || args.length === 1 && args[0] === undefined ? undefined : args as RefreshArgs),
  117. refresh: (...args) => refreshValue(true, args),
  118. memoize,
  119. };
  120. }