self-refreshing-cache.ts 4.7 KB

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