1
0

redis-cache-strategy.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import { JsonCompatible } from '@vendure/common/lib/shared-types';
  2. import { Logger } from '../../config/logger/vendure-logger';
  3. import { CacheStrategy, SetCacheKeyOptions } from '../../config/system/cache-strategy';
  4. import { DEFAULT_NAMESPACE, DEFAULT_TTL, loggerCtx } from './constants';
  5. import { RedisCachePluginInitOptions } from './types';
  6. /**
  7. * @description
  8. * A {@link CacheStrategy} which stores cached items in a Redis instance.
  9. * This is a high-performance cache strategy which is suitable for production use.
  10. *
  11. * @docsCategory cache
  12. * @since 3.1.0
  13. */
  14. export class RedisCacheStrategy implements CacheStrategy {
  15. private client: import('ioredis').Redis;
  16. constructor(private options: RedisCachePluginInitOptions) {}
  17. async init() {
  18. const IORedis = await import('ioredis').then(m => m.default);
  19. this.client = new IORedis.Redis(this.options.redisOptions ?? {});
  20. this.client.on('error', err => Logger.error(err.message, loggerCtx, err.stack));
  21. }
  22. async destroy() {
  23. await this.client.quit();
  24. }
  25. async get<T extends JsonCompatible<T>>(key: string): Promise<T | undefined> {
  26. try {
  27. const retrieved = await this.client.get(this.namespace(key));
  28. if (retrieved) {
  29. try {
  30. return JSON.parse(retrieved);
  31. } catch (e: any) {
  32. Logger.error(`Could not parse cache item ${key}: ${e.message as string}`, loggerCtx);
  33. }
  34. }
  35. } catch (e: any) {
  36. Logger.error(`Could not get cache item ${key}: ${e.message as string}`, loggerCtx);
  37. }
  38. }
  39. async set<T extends JsonCompatible<T>>(
  40. key: string,
  41. value: T,
  42. options?: SetCacheKeyOptions,
  43. ): Promise<void> {
  44. try {
  45. const multi = this.client.multi();
  46. const ttl = options?.ttl ? options.ttl / 1000 : DEFAULT_TTL;
  47. const namedspacedKey = this.namespace(key);
  48. const serializedValue = JSON.stringify(value);
  49. if (this.options.maxItemSizeInBytes) {
  50. if (Buffer.byteLength(serializedValue) > this.options.maxItemSizeInBytes) {
  51. Logger.error(
  52. `Could not set cache item ${key}: item size of ${Buffer.byteLength(
  53. serializedValue,
  54. )} bytes exceeds maxItemSizeInBytes of ${this.options.maxItemSizeInBytes} bytes`,
  55. loggerCtx,
  56. );
  57. return;
  58. }
  59. }
  60. multi.set(namedspacedKey, JSON.stringify(value), 'EX', ttl);
  61. if (options?.tags) {
  62. for (const tag of options.tags) {
  63. multi.sadd(this.tagNamespace(tag), namedspacedKey);
  64. }
  65. }
  66. await multi.exec();
  67. } catch (e: any) {
  68. Logger.error(`Could not set cache item ${key}: ${e.message as string}`, loggerCtx);
  69. }
  70. }
  71. async delete(key: string): Promise<void> {
  72. try {
  73. await this.client.del(this.namespace(key));
  74. } catch (e: any) {
  75. Logger.error(`Could not delete cache item ${key}: ${e.message as string}`, loggerCtx);
  76. }
  77. }
  78. async invalidateTags(tags: string[]): Promise<void> {
  79. try {
  80. const keys = [
  81. ...(await Promise.all(tags.map(tag => this.client.smembers(this.tagNamespace(tag))))),
  82. ];
  83. const pipeline = this.client.pipeline();
  84. keys.forEach(key => {
  85. pipeline.del(key);
  86. });
  87. tags.forEach(tag => {
  88. const namespacedTag = this.tagNamespace(tag);
  89. pipeline.del(namespacedTag);
  90. });
  91. await pipeline.exec();
  92. } catch (err) {
  93. return Promise.reject(err);
  94. }
  95. }
  96. private namespace(key: string) {
  97. return `${this.options.namespace ?? DEFAULT_NAMESPACE}:${key}`;
  98. }
  99. private tagNamespace(tag: string) {
  100. return `${this.options.namespace ?? DEFAULT_NAMESPACE}:tag:${tag}`;
  101. }
  102. }