memory-cache-store.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. 'use strict'
  2. const { Writable } = require('node:stream')
  3. const { EventEmitter } = require('node:events')
  4. const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
  5. /**
  6. * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey
  7. * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheValue} CacheValue
  8. * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
  9. * @typedef {import('../../types/cache-interceptor.d.ts').default.GetResult} GetResult
  10. */
  11. /**
  12. * @implements {CacheStore}
  13. * @extends {EventEmitter}
  14. */
  15. class MemoryCacheStore extends EventEmitter {
  16. #maxCount = 1024
  17. #maxSize = 104857600 // 100MB
  18. #maxEntrySize = 5242880 // 5MB
  19. #size = 0
  20. #count = 0
  21. #entries = new Map()
  22. #hasEmittedMaxSizeEvent = false
  23. /**
  24. * @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts]
  25. */
  26. constructor (opts) {
  27. super()
  28. if (opts) {
  29. if (typeof opts !== 'object') {
  30. throw new TypeError('MemoryCacheStore options must be an object')
  31. }
  32. if (opts.maxCount !== undefined) {
  33. if (
  34. typeof opts.maxCount !== 'number' ||
  35. !Number.isInteger(opts.maxCount) ||
  36. opts.maxCount < 0
  37. ) {
  38. throw new TypeError('MemoryCacheStore options.maxCount must be a non-negative integer')
  39. }
  40. this.#maxCount = opts.maxCount
  41. }
  42. if (opts.maxSize !== undefined) {
  43. if (
  44. typeof opts.maxSize !== 'number' ||
  45. !Number.isInteger(opts.maxSize) ||
  46. opts.maxSize < 0
  47. ) {
  48. throw new TypeError('MemoryCacheStore options.maxSize must be a non-negative integer')
  49. }
  50. this.#maxSize = opts.maxSize
  51. }
  52. if (opts.maxEntrySize !== undefined) {
  53. if (
  54. typeof opts.maxEntrySize !== 'number' ||
  55. !Number.isInteger(opts.maxEntrySize) ||
  56. opts.maxEntrySize < 0
  57. ) {
  58. throw new TypeError('MemoryCacheStore options.maxEntrySize must be a non-negative integer')
  59. }
  60. this.#maxEntrySize = opts.maxEntrySize
  61. }
  62. }
  63. }
  64. /**
  65. * Get the current size of the cache in bytes
  66. * @returns {number} The current size of the cache in bytes
  67. */
  68. get size () {
  69. return this.#size
  70. }
  71. /**
  72. * Check if the cache is full (either max size or max count reached)
  73. * @returns {boolean} True if the cache is full, false otherwise
  74. */
  75. isFull () {
  76. return this.#size >= this.#maxSize || this.#count >= this.#maxCount
  77. }
  78. /**
  79. * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req
  80. * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
  81. */
  82. get (key) {
  83. assertCacheKey(key)
  84. const topLevelKey = `${key.origin}:${key.path}`
  85. const now = Date.now()
  86. const entries = this.#entries.get(topLevelKey)
  87. const entry = entries ? findEntry(key, entries, now) : null
  88. return entry == null
  89. ? undefined
  90. : {
  91. statusMessage: entry.statusMessage,
  92. statusCode: entry.statusCode,
  93. headers: entry.headers,
  94. body: entry.body,
  95. vary: entry.vary ? entry.vary : undefined,
  96. etag: entry.etag,
  97. cacheControlDirectives: entry.cacheControlDirectives,
  98. cachedAt: entry.cachedAt,
  99. staleAt: entry.staleAt,
  100. deleteAt: entry.deleteAt
  101. }
  102. }
  103. /**
  104. * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
  105. * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} val
  106. * @returns {Writable | undefined}
  107. */
  108. createWriteStream (key, val) {
  109. assertCacheKey(key)
  110. assertCacheValue(val)
  111. const topLevelKey = `${key.origin}:${key.path}`
  112. const store = this
  113. const entry = { ...key, ...val, body: [], size: 0 }
  114. return new Writable({
  115. write (chunk, encoding, callback) {
  116. if (typeof chunk === 'string') {
  117. chunk = Buffer.from(chunk, encoding)
  118. }
  119. entry.size += chunk.byteLength
  120. if (entry.size >= store.#maxEntrySize) {
  121. this.destroy()
  122. } else {
  123. entry.body.push(chunk)
  124. }
  125. callback(null)
  126. },
  127. final (callback) {
  128. let entries = store.#entries.get(topLevelKey)
  129. if (!entries) {
  130. entries = []
  131. store.#entries.set(topLevelKey, entries)
  132. }
  133. const previousEntry = findEntry(key, entries, Date.now())
  134. if (previousEntry) {
  135. const index = entries.indexOf(previousEntry)
  136. entries.splice(index, 1, entry)
  137. store.#size -= previousEntry.size
  138. } else {
  139. entries.push(entry)
  140. store.#count += 1
  141. }
  142. store.#size += entry.size
  143. // Check if cache is full and emit event if needed
  144. if (store.#size > store.#maxSize || store.#count > store.#maxCount) {
  145. // Emit maxSizeExceeded event if we haven't already
  146. if (!store.#hasEmittedMaxSizeEvent) {
  147. store.emit('maxSizeExceeded', {
  148. size: store.#size,
  149. maxSize: store.#maxSize,
  150. count: store.#count,
  151. maxCount: store.#maxCount
  152. })
  153. store.#hasEmittedMaxSizeEvent = true
  154. }
  155. // Perform eviction
  156. for (const [key, entries] of store.#entries) {
  157. for (const entry of entries.splice(0, entries.length / 2)) {
  158. store.#size -= entry.size
  159. store.#count -= 1
  160. }
  161. if (entries.length === 0) {
  162. store.#entries.delete(key)
  163. }
  164. }
  165. // Reset the event flag after eviction
  166. if (store.#size < store.#maxSize && store.#count < store.#maxCount) {
  167. store.#hasEmittedMaxSizeEvent = false
  168. }
  169. }
  170. callback(null)
  171. }
  172. })
  173. }
  174. /**
  175. * @param {CacheKey} key
  176. */
  177. delete (key) {
  178. if (typeof key !== 'object') {
  179. throw new TypeError(`expected key to be object, got ${typeof key}`)
  180. }
  181. const topLevelKey = `${key.origin}:${key.path}`
  182. for (const entry of this.#entries.get(topLevelKey) ?? []) {
  183. this.#size -= entry.size
  184. this.#count -= 1
  185. }
  186. this.#entries.delete(topLevelKey)
  187. }
  188. }
  189. function findEntry (key, entries, now) {
  190. return entries.find((entry) => (
  191. entry.deleteAt > now &&
  192. entry.method === key.method &&
  193. (entry.vary == null || Object.keys(entry.vary).every(headerName => {
  194. if (entry.vary[headerName] === null) {
  195. return key.headers[headerName] === undefined
  196. }
  197. return entry.vary[headerName] === key.headers[headerName]
  198. }))
  199. ))
  200. }
  201. module.exports = MemoryCacheStore