self-refreshing-cache.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import { Logger } from '../config/logger/vendure-logger';
  2. /**
  3. * @description
  4. * A cache which automatically refreshes itself if the value is found to be stale.
  5. *
  6. * @docsCategory cache
  7. * @docsPage SelfRefreshingCache
  8. */
  9. export interface SelfRefreshingCache<V, RefreshArgs extends any[] = []> {
  10. /**
  11. * @description
  12. * The current value of the cache. If the value is stale, the data will be refreshed and then
  13. * the fresh value will be returned.
  14. */
  15. value(...refreshArgs: RefreshArgs | [undefined] | []): Promise<V>;
  16. /**
  17. * @description
  18. * Allows a memoized function to be defined. For the given arguments, the `fn` function will
  19. * be invoked only once and its output cached and returned.
  20. * The results cache is cleared along with the rest of the cache according to the configured
  21. * `ttl` value.
  22. */
  23. memoize<Args extends any[], R>(
  24. args: Args,
  25. refreshArgs: RefreshArgs,
  26. fn: (value: V, ...args: Args) => R,
  27. ): Promise<R>;
  28. /**
  29. * @description
  30. * Force a refresh of the value, e.g. when it is known that the value has changed such as after
  31. * an update operation to the source data in the database.
  32. */
  33. refresh(...args: RefreshArgs): Promise<V>;
  34. }
  35. /**
  36. * @description
  37. * Configuration options for creating a {@link SelfRefreshingCache}.
  38. *
  39. * @docsCategory cache
  40. * @docsPage SelfRefreshingCache
  41. */
  42. export interface SelfRefreshingCacheConfig<V, RefreshArgs extends any[]> {
  43. /**
  44. * @description
  45. * The name of the cache, used for logging purposes.
  46. * e.g. `'MyService.cachedValue'`.
  47. */
  48. name: string;
  49. /**
  50. * @description
  51. * The time-to-live (ttl) in milliseconds for the cache. After this time, the value will be considered stale
  52. * and will be refreshed the next time it is accessed.
  53. */
  54. ttl: number;
  55. /**
  56. * @description
  57. * The function which is used to refresh the value of the cache.
  58. * This function should return a Promise which resolves to the new value.
  59. */
  60. refresh: {
  61. fn: (...args: RefreshArgs) => Promise<V>;
  62. /**
  63. * Default arguments, passed to refresh function
  64. */
  65. defaultArgs: RefreshArgs;
  66. };
  67. /**
  68. * @description
  69. * Intended for unit testing the SelfRefreshingCache only.
  70. * By default uses `() => new Date().getTime()`
  71. */
  72. getTimeFn?: () => number;
  73. }
  74. /**
  75. * @description
  76. * Creates a {@link SelfRefreshingCache} object, which is used to cache a single frequently-accessed value. In this type
  77. * of cache, the function used to populate the value (`refreshFn`) is defined during the creation of the cache, and
  78. * it is immediately used to populate the initial value.
  79. *
  80. * From there, when the `.value` property is accessed, it will return a value from the cache, and if the
  81. * value has expired, it will automatically run the `refreshFn` to update the value and then return the
  82. * fresh value.
  83. *
  84. * @example
  85. * ```ts title="Example of creating a SelfRefreshingCache"
  86. * import { createSelfRefreshingCache } from '@vendure/core';
  87. *
  88. * \@Injectable()
  89. * export class PublicChannelService {
  90. * private publicChannel: SelfRefreshingCache<Channel, [RequestContext]>;
  91. *
  92. * async init() {
  93. * this.publicChannel = await createSelfRefreshingCache<Channel, [RequestContext]>({
  94. * name: 'PublicChannelService.publicChannel',
  95. * ttl: 1000 * 60 * 60, // 1 hour
  96. * refresh: {
  97. * fn: async (ctx: RequestContext) => {
  98. * return this.channelService.getPublicChannel(ctx);
  99. * },
  100. * defaultArgs: [RequestContext.empty()],
  101. * },
  102. * });
  103. * }
  104. * ```
  105. *
  106. * @docsCategory cache
  107. * @docsPage SelfRefreshingCache
  108. */
  109. export async function createSelfRefreshingCache<V, RefreshArgs extends any[]>(
  110. config: SelfRefreshingCacheConfig<V, RefreshArgs>,
  111. refreshArgs?: RefreshArgs,
  112. ): Promise<SelfRefreshingCache<V, RefreshArgs>> {
  113. const { ttl, name, refresh, getTimeFn } = config;
  114. const getTimeNow = getTimeFn ?? (() => new Date().getTime());
  115. const initialValue: V = await refresh.fn(...(refreshArgs ?? refresh.defaultArgs));
  116. let value = initialValue;
  117. let expires = getTimeNow() + ttl;
  118. const memoCache = new Map<string, { expires: number; value: any }>();
  119. const refreshValue = (resetMemoCache = true, args: RefreshArgs): Promise<V> => {
  120. return refresh
  121. .fn(...args)
  122. .then(newValue => {
  123. value = newValue;
  124. expires = getTimeNow() + ttl;
  125. if (resetMemoCache) {
  126. memoCache.clear();
  127. }
  128. return value;
  129. })
  130. .catch((err: any) => {
  131. const _message = err.message;
  132. const message = typeof _message === 'string' ? _message : JSON.stringify(err.message);
  133. Logger.error(
  134. `Failed to update SelfRefreshingCache "${name}": ${message}`,
  135. undefined,
  136. err.stack,
  137. );
  138. return value;
  139. });
  140. };
  141. const getValue = async (_refreshArgs?: RefreshArgs, resetMemoCache = true): Promise<V> => {
  142. const now = getTimeNow();
  143. if (expires < now) {
  144. return refreshValue(resetMemoCache, _refreshArgs ?? refresh.defaultArgs);
  145. }
  146. return value;
  147. };
  148. const memoize = async <Args extends any[], R>(
  149. args: Args,
  150. _refreshArgs: RefreshArgs,
  151. fn: (value: V, ...args: Args) => R,
  152. ): Promise<R> => {
  153. const key = JSON.stringify(args);
  154. const cached = memoCache.get(key);
  155. const now = getTimeNow();
  156. if (cached && now < cached.expires) {
  157. return cached.value;
  158. }
  159. const result = getValue(_refreshArgs, false).then(val => fn(val, ...args));
  160. memoCache.set(key, {
  161. expires: now + ttl,
  162. value: result,
  163. });
  164. return result;
  165. };
  166. return {
  167. value: (...args) =>
  168. getValue(
  169. !args.length || (args.length === 1 && args[0] === undefined)
  170. ? undefined
  171. : (args as RefreshArgs),
  172. ),
  173. refresh: (...args) => refreshValue(true, args),
  174. memoize,
  175. };
  176. }