sql-cache-strategy.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import { JsonCompatible } from '@vendure/common/lib/shared-types';
  2. import { Injector } from '../../common/index';
  3. import { ConfigService, Logger } from '../../config/index';
  4. import { CacheStrategy, SetCacheKeyOptions } from '../../config/system/cache-strategy';
  5. import { TransactionalConnection } from '../../connection/index';
  6. import { CacheItem } from './cache-item.entity';
  7. /**
  8. * A {@link CacheStrategy} that stores the cache in memory using a simple
  9. * JavaScript Map.
  10. *
  11. * **Caution** do not use this in a multi-instance deployment because
  12. * cache invalidation will not propagate to other instances.
  13. *
  14. * @since 3.1.0
  15. */
  16. export class SqlCacheStrategy implements CacheStrategy {
  17. protected cacheSize = 10_000;
  18. constructor(config?: { cacheSize?: number }) {
  19. if (config?.cacheSize) {
  20. this.cacheSize = config.cacheSize;
  21. }
  22. }
  23. protected connection: TransactionalConnection;
  24. protected configService: ConfigService;
  25. init(injector: Injector) {
  26. this.connection = injector.get(TransactionalConnection);
  27. this.configService = injector.get(ConfigService);
  28. }
  29. async get<T extends JsonCompatible<T>>(key: string): Promise<T | undefined> {
  30. const hit = await this.connection.rawConnection.getRepository(CacheItem).findOne({
  31. where: {
  32. key,
  33. },
  34. });
  35. if (hit) {
  36. const now = new Date().getTime();
  37. if (!hit.expiresAt || (hit.expiresAt && now < hit.expiresAt.getTime())) {
  38. try {
  39. return JSON.parse(hit.value);
  40. } catch (e: any) {
  41. /* */
  42. }
  43. } else {
  44. await this.connection.rawConnection.getRepository(CacheItem).delete({
  45. key,
  46. });
  47. }
  48. }
  49. }
  50. async set<T extends JsonCompatible<T>>(key: string, value: T, options?: SetCacheKeyOptions) {
  51. const cacheSize = await this.connection.rawConnection.getRepository(CacheItem).count();
  52. if (cacheSize > this.cacheSize) {
  53. // evict oldest
  54. const subQuery1 = this.connection.rawConnection
  55. .getRepository(CacheItem)
  56. .createQueryBuilder('item')
  57. .select('item.id', 'item_id')
  58. .orderBy('item.updatedAt', 'DESC')
  59. .limit(1000)
  60. .offset(this.cacheSize);
  61. const subQuery2 = this.connection.rawConnection
  62. .createQueryBuilder()
  63. .select('t.item_id')
  64. .from(`(${subQuery1.getQuery()})`, 't');
  65. const qb = this.connection.rawConnection
  66. .getRepository(CacheItem)
  67. .createQueryBuilder('cache_item')
  68. .delete()
  69. .from(CacheItem, 'cache_item')
  70. .where(`cache_item.id IN (${subQuery2.getQuery()})`);
  71. try {
  72. await qb.execute();
  73. } catch (e: any) {
  74. Logger.error(`An error occured when attempting to prune the cache: ${e.message as string}`);
  75. }
  76. }
  77. await this.connection.rawConnection.getRepository(CacheItem).upsert(
  78. new CacheItem({
  79. key,
  80. value: JSON.stringify(value),
  81. expiresAt: options?.ttl ? new Date(new Date().getTime() + options.ttl) : undefined,
  82. }),
  83. ['key'],
  84. );
  85. }
  86. async delete(key: string) {
  87. await this.connection.rawConnection.getRepository(CacheItem).delete({
  88. key,
  89. });
  90. }
  91. }