cache.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { Redis } from "@upstash/redis";
  2. import { Cache } from "../core/index.js";
  3. import { entityKind, is } from "../../entity.js";
  4. import { OriginalName, Table } from "../../index.js";
  5. const getByTagScript = `
  6. local tagsMapKey = KEYS[1] -- tags map key
  7. local tag = ARGV[1] -- tag
  8. local compositeTableName = redis.call('HGET', tagsMapKey, tag)
  9. if not compositeTableName then
  10. return nil
  11. end
  12. local value = redis.call('HGET', compositeTableName, tag)
  13. return value
  14. `;
  15. const onMutateScript = `
  16. local tagsMapKey = KEYS[1] -- tags map key
  17. local tables = {} -- initialize tables array
  18. local tags = ARGV -- tags array
  19. for i = 2, #KEYS do
  20. tables[#tables + 1] = KEYS[i] -- add all keys except the first one to tables
  21. end
  22. if #tags > 0 then
  23. for _, tag in ipairs(tags) do
  24. if tag ~= nil and tag ~= '' then
  25. local compositeTableName = redis.call('HGET', tagsMapKey, tag)
  26. if compositeTableName then
  27. redis.call('HDEL', compositeTableName, tag)
  28. end
  29. end
  30. end
  31. redis.call('HDEL', tagsMapKey, unpack(tags))
  32. end
  33. local keysToDelete = {}
  34. if #tables > 0 then
  35. local compositeTableNames = redis.call('SUNION', unpack(tables))
  36. for _, compositeTableName in ipairs(compositeTableNames) do
  37. keysToDelete[#keysToDelete + 1] = compositeTableName
  38. end
  39. for _, table in ipairs(tables) do
  40. keysToDelete[#keysToDelete + 1] = table
  41. end
  42. redis.call('DEL', unpack(keysToDelete))
  43. end
  44. `;
  45. class UpstashCache extends Cache {
  46. constructor(redis, config, useGlobally) {
  47. super();
  48. this.redis = redis;
  49. this.useGlobally = useGlobally;
  50. this.internalConfig = this.toInternalConfig(config);
  51. this.luaScripts = {
  52. getByTagScript: this.redis.createScript(getByTagScript, { readonly: true }),
  53. onMutateScript: this.redis.createScript(onMutateScript)
  54. };
  55. }
  56. static [entityKind] = "UpstashCache";
  57. /**
  58. * Prefix for sets which denote the composite table names for each unique table
  59. *
  60. * Example: In the composite table set of "table1", you may find
  61. * `${compositeTablePrefix}table1,table2` and `${compositeTablePrefix}table1,table3`
  62. */
  63. static compositeTableSetPrefix = "__CTS__";
  64. /**
  65. * Prefix for hashes which map hash or tags to cache values
  66. */
  67. static compositeTablePrefix = "__CT__";
  68. /**
  69. * Key which holds the mapping of tags to composite table names
  70. *
  71. * Using this tagsMapKey, you can find the composite table name for a given tag
  72. * and get the cache value for that tag:
  73. *
  74. * ```ts
  75. * const compositeTable = redis.hget(tagsMapKey, 'tag1')
  76. * console.log(compositeTable) // `${compositeTablePrefix}table1,table2`
  77. *
  78. * const cachevalue = redis.hget(compositeTable, 'tag1')
  79. */
  80. static tagsMapKey = "__tagsMap__";
  81. /**
  82. * Queries whose auto invalidation is false aren't stored in their respective
  83. * composite table hashes because those hashes are deleted when a mutation
  84. * occurs on related tables.
  85. *
  86. * Instead, they are stored in a separate hash with the prefix
  87. * `__nonAutoInvalidate__` to prevent them from being deleted when a mutation
  88. */
  89. static nonAutoInvalidateTablePrefix = "__nonAutoInvalidate__";
  90. luaScripts;
  91. internalConfig;
  92. strategy() {
  93. return this.useGlobally ? "all" : "explicit";
  94. }
  95. toInternalConfig(config) {
  96. return config ? {
  97. seconds: config.ex,
  98. hexOptions: config.hexOptions
  99. } : {
  100. seconds: 1
  101. };
  102. }
  103. async get(key, tables, isTag = false, isAutoInvalidate) {
  104. if (!isAutoInvalidate) {
  105. const result2 = await this.redis.hget(UpstashCache.nonAutoInvalidateTablePrefix, key);
  106. return result2 === null ? void 0 : result2;
  107. }
  108. if (isTag) {
  109. const result2 = await this.luaScripts.getByTagScript.exec([UpstashCache.tagsMapKey], [key]);
  110. return result2 === null ? void 0 : result2;
  111. }
  112. const compositeKey = this.getCompositeKey(tables);
  113. const result = (await this.redis.hget(compositeKey, key)) ?? void 0;
  114. return result === null ? void 0 : result;
  115. }
  116. async put(key, response, tables, isTag = false, config) {
  117. const isAutoInvalidate = tables.length !== 0;
  118. const pipeline = this.redis.pipeline();
  119. const ttlSeconds = config && config.ex ? config.ex : this.internalConfig.seconds;
  120. const hexOptions = config && config.hexOptions ? config.hexOptions : this.internalConfig?.hexOptions;
  121. if (!isAutoInvalidate) {
  122. if (isTag) {
  123. pipeline.hset(UpstashCache.tagsMapKey, { [key]: UpstashCache.nonAutoInvalidateTablePrefix });
  124. pipeline.hexpire(UpstashCache.tagsMapKey, key, ttlSeconds, hexOptions);
  125. }
  126. pipeline.hset(UpstashCache.nonAutoInvalidateTablePrefix, { [key]: response });
  127. pipeline.hexpire(UpstashCache.nonAutoInvalidateTablePrefix, key, ttlSeconds, hexOptions);
  128. await pipeline.exec();
  129. return;
  130. }
  131. const compositeKey = this.getCompositeKey(tables);
  132. pipeline.hset(compositeKey, { [key]: response });
  133. pipeline.hexpire(compositeKey, key, ttlSeconds, hexOptions);
  134. if (isTag) {
  135. pipeline.hset(UpstashCache.tagsMapKey, { [key]: compositeKey });
  136. pipeline.hexpire(UpstashCache.tagsMapKey, key, ttlSeconds, hexOptions);
  137. }
  138. for (const table of tables) {
  139. pipeline.sadd(this.addTablePrefix(table), compositeKey);
  140. }
  141. await pipeline.exec();
  142. }
  143. async onMutate(params) {
  144. const tags = Array.isArray(params.tags) ? params.tags : params.tags ? [params.tags] : [];
  145. const tables = Array.isArray(params.tables) ? params.tables : params.tables ? [params.tables] : [];
  146. const tableNames = tables.map((table) => is(table, Table) ? table[OriginalName] : table);
  147. const compositeTableSets = tableNames.map((table) => this.addTablePrefix(table));
  148. await this.luaScripts.onMutateScript.exec([UpstashCache.tagsMapKey, ...compositeTableSets], tags);
  149. }
  150. addTablePrefix = (table) => `${UpstashCache.compositeTableSetPrefix}${table}`;
  151. getCompositeKey = (tables) => `${UpstashCache.compositeTablePrefix}${tables.sort().join(",")}`;
  152. }
  153. function upstashCache({ url, token, config, global = false }) {
  154. const redis = new Redis({
  155. url,
  156. token
  157. });
  158. return new UpstashCache(redis, config, global);
  159. }
  160. export {
  161. UpstashCache,
  162. upstashCache
  163. };
  164. //# sourceMappingURL=cache.js.map