cache/scopedCache.js

  1. import {getCache} from '../cache.js';
  2. import {reusePromiseForever} from '../reusePromises.js';
  3. /**
  4. * A wrapper class for accessing the cache.
  5. * Prefixes a string before each key to avoid conflicts.
  6. */
  7. export class ScopedCache {
  8. /**
  9. * Create a new ScopedCache object by passing in the key prefix descired
  10. * @param {string} keyPrefix
  11. * @param {number} [version] Cache version, bump this to invalidate existing cache entries for a scope
  12. * @param {object} [cacheObject=null] Manually supply a cache object to use
  13. */
  14. constructor(keyPrefix, version = 0, cacheObject = null) {
  15. this.prefix = keyPrefix;
  16. this.version = version;
  17. this.cache = null;
  18. this.cacheOverride = cacheObject;
  19. }
  20. /**
  21. * Initialise the cache for this scope
  22. */
  23. async initCache() {
  24. return await reusePromiseForever(this, this._initCache);
  25. }
  26. /**
  27. * Internal cache initialisation
  28. */
  29. async _initCache() {
  30. if (this.cache) {
  31. return this.cache;
  32. }
  33. if (!!this.cacheOverride) {
  34. this.cache = this.cacheOverride;
  35. } else {
  36. this.cache = await getCache();
  37. }
  38. // check and flush cache if version mismatch
  39. const cacheVersion = await this.cache.get(this.generateScopedKey('%%version%%'));
  40. if (cacheVersion !== undefined && cacheVersion != this.version) {
  41. // find all cache entries with this scope, and remove them
  42. const keys = await this.cache.getKeys(`${this.prefix}_`);
  43. await Promise.allSettled(keys.map((key) => {
  44. // set expire date to 1 millisecond ago (this basically deletes it)
  45. return this.cache.set(key, {}, -1);
  46. }));
  47. }
  48. // set our new cache version with a very very long ttl
  49. await this.cache.set(this.generateScopedKey('%%version%%'), this.version, Number.MAX_SAFE_INTEGER);
  50. this._initCachePromise = null;
  51. return this.cache;
  52. }
  53. /**
  54. * Generate a scoped key by adding our prefix to the incoming key
  55. * @param {string} inKey
  56. * @return {string} Scoped key
  57. */
  58. generateScopedKey(inKey) {
  59. return `${this.prefix}_${inKey}`;
  60. }
  61. /**
  62. * Get a cached object
  63. * @public
  64. * @async
  65. * @param {string} key Unique key name for this cache entry
  66. * @param {boolean} [getFullObject] Get the full cache entry, including expiry time, even if expired
  67. * @param {boolean} [force=false] Force set, bypassing transaction blocks
  68. * @return {(Object|undefined)} Returns the object in the cache, or undefined if not present
  69. */
  70. async get(key, getFullObject = false, force = false) {
  71. return this.getGlobal(this.generateScopedKey(key), getFullObject, force);
  72. }
  73. /**
  74. * Get a cached object from the global cache (skipping the scope prefix)
  75. * @public
  76. * @async
  77. * @param {string} key Unique key name for this cache entry
  78. * @param {boolean} [getFullObject] Get the full cache entry, including expiry time, even if expired
  79. * @param {boolean} [force=false] Force set, bypassing transaction blocks
  80. * @return {(Object|undefined)} Returns the object in the cache, or undefined if not present
  81. */
  82. async getGlobal(key, getFullObject = false, force = false) {
  83. const cache = await this.initCache();
  84. return cache.get(key, getFullObject, force);
  85. }
  86. /**
  87. * Set a key in our cache
  88. * @public
  89. * @async
  90. * @param {string} key Unique key name for this cache entry
  91. * @param {Object} value
  92. * @param {(Function|number)} [ttl=3600000] How long the cache entry should last in milliseconds
  93. * Can be a number or a function that will return a number
  94. * Default 1 hour
  95. */
  96. async set(key, value, ttl = 3600000) {
  97. return this.setGlobal(this.generateScopedKey(key), value, ttl);
  98. }
  99. /**
  100. * Set a key in our global cache, skipping the scoped prefix
  101. * @public
  102. * @async
  103. * @param {string} key Unique key name for this cache entry
  104. * @param {Object} value
  105. * @param {(Function|number)} [ttl=3600000] How long the cache entry should last in milliseconds
  106. * Can be a number or a function that will return a number
  107. * Default 1 hour
  108. */
  109. async setGlobal(key, value, ttl = 3600000) {
  110. const cache = await this.initCache();
  111. return cache.set(key, value, ttl);
  112. }
  113. /**
  114. * A helper "wrap" function that will return a cached value if present
  115. * This will call the supplied function to fetch it if the value isn't present in the cache
  116. * @public
  117. * @async
  118. * @param {string} key Unique key name for this cache entry
  119. * @param {function} fn Fetch function that will be called if the cache entry is not present
  120. * @param {(function|number)} [ttl] How long the cache entry should last in milliseconds
  121. * Can be a number or a function that will return a number
  122. */
  123. async wrap(key, fn, ttl) {
  124. return this.wrapGlobal(this.generateScopedKey(key), fn, ttl);
  125. }
  126. /**
  127. * A helper "wrap" function that will return a cached value if present (in the global scope)
  128. * This will call the supplied function to fetch it if the value isn't present in the cache
  129. * @public
  130. * @async
  131. * @param {string} key Unique key name for this cache entry
  132. * @param {function} fn Fetch function that will be called if the cache entry is not present
  133. * @param {(function|number)} [ttl] How long the cache entry should last in milliseconds
  134. * Can be a number or a function that will return a number
  135. */
  136. async wrapGlobal(key, fn, ttl) {
  137. const cache = await this.initCache();
  138. return cache.wrap(key, fn, ttl);
  139. }
  140. /**
  141. * Run a series of functions in a single transaction
  142. * @param {functions} func
  143. * @return {promise}
  144. */
  145. async runTransaction(func) {
  146. const cache = await this.initCache();
  147. return cache.runTransaction(func, (key) => {
  148. return this.generateScopedKey(key);
  149. });
  150. }
  151. /**
  152. * Block if we have any pending transactions
  153. * @return {Promise}
  154. */
  155. async blockOnPendingTransactions() {
  156. const cache = await this.initCache();
  157. return cache.blockOnPendingTransactions();
  158. }
  159. /**
  160. * Get an array of all the cached keys matching the supplied prefix
  161. * @param {string} [prefix='']
  162. * @return {array<string>}
  163. */
  164. async getKeys(prefix = '') {
  165. const cache = await this.initCache();
  166. return (await cache.getKeys(`${this.prefix}_${prefix}`)).map((x) => {
  167. // return keys without our scoped cache prefix
  168. return x.slice(this.prefix.length + 1);
  169. });
  170. }
  171. }
  172. export default ScopedCache;