123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- 'use strict'
- const { Writable } = require('node:stream')
- const { EventEmitter } = require('node:events')
- const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
- /**
- * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey
- * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheValue} CacheValue
- * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
- * @typedef {import('../../types/cache-interceptor.d.ts').default.GetResult} GetResult
- */
- /**
- * @implements {CacheStore}
- * @extends {EventEmitter}
- */
- class MemoryCacheStore extends EventEmitter {
- #maxCount = 1024
- #maxSize = 104857600 // 100MB
- #maxEntrySize = 5242880 // 5MB
- #size = 0
- #count = 0
- #entries = new Map()
- #hasEmittedMaxSizeEvent = false
- /**
- * @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts]
- */
- constructor (opts) {
- super()
- if (opts) {
- if (typeof opts !== 'object') {
- throw new TypeError('MemoryCacheStore options must be an object')
- }
- if (opts.maxCount !== undefined) {
- if (
- typeof opts.maxCount !== 'number' ||
- !Number.isInteger(opts.maxCount) ||
- opts.maxCount < 0
- ) {
- throw new TypeError('MemoryCacheStore options.maxCount must be a non-negative integer')
- }
- this.#maxCount = opts.maxCount
- }
- if (opts.maxSize !== undefined) {
- if (
- typeof opts.maxSize !== 'number' ||
- !Number.isInteger(opts.maxSize) ||
- opts.maxSize < 0
- ) {
- throw new TypeError('MemoryCacheStore options.maxSize must be a non-negative integer')
- }
- this.#maxSize = opts.maxSize
- }
- if (opts.maxEntrySize !== undefined) {
- if (
- typeof opts.maxEntrySize !== 'number' ||
- !Number.isInteger(opts.maxEntrySize) ||
- opts.maxEntrySize < 0
- ) {
- throw new TypeError('MemoryCacheStore options.maxEntrySize must be a non-negative integer')
- }
- this.#maxEntrySize = opts.maxEntrySize
- }
- }
- }
- /**
- * Get the current size of the cache in bytes
- * @returns {number} The current size of the cache in bytes
- */
- get size () {
- return this.#size
- }
- /**
- * Check if the cache is full (either max size or max count reached)
- * @returns {boolean} True if the cache is full, false otherwise
- */
- isFull () {
- return this.#size >= this.#maxSize || this.#count >= this.#maxCount
- }
- /**
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req
- * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
- */
- get (key) {
- assertCacheKey(key)
- const topLevelKey = `${key.origin}:${key.path}`
- const now = Date.now()
- const entries = this.#entries.get(topLevelKey)
- const entry = entries ? findEntry(key, entries, now) : null
- return entry == null
- ? undefined
- : {
- statusMessage: entry.statusMessage,
- statusCode: entry.statusCode,
- headers: entry.headers,
- body: entry.body,
- vary: entry.vary ? entry.vary : undefined,
- etag: entry.etag,
- cacheControlDirectives: entry.cacheControlDirectives,
- cachedAt: entry.cachedAt,
- staleAt: entry.staleAt,
- deleteAt: entry.deleteAt
- }
- }
- /**
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} val
- * @returns {Writable | undefined}
- */
- createWriteStream (key, val) {
- assertCacheKey(key)
- assertCacheValue(val)
- const topLevelKey = `${key.origin}:${key.path}`
- const store = this
- const entry = { ...key, ...val, body: [], size: 0 }
- return new Writable({
- write (chunk, encoding, callback) {
- if (typeof chunk === 'string') {
- chunk = Buffer.from(chunk, encoding)
- }
- entry.size += chunk.byteLength
- if (entry.size >= store.#maxEntrySize) {
- this.destroy()
- } else {
- entry.body.push(chunk)
- }
- callback(null)
- },
- final (callback) {
- let entries = store.#entries.get(topLevelKey)
- if (!entries) {
- entries = []
- store.#entries.set(topLevelKey, entries)
- }
- const previousEntry = findEntry(key, entries, Date.now())
- if (previousEntry) {
- const index = entries.indexOf(previousEntry)
- entries.splice(index, 1, entry)
- store.#size -= previousEntry.size
- } else {
- entries.push(entry)
- store.#count += 1
- }
- store.#size += entry.size
- // Check if cache is full and emit event if needed
- if (store.#size > store.#maxSize || store.#count > store.#maxCount) {
- // Emit maxSizeExceeded event if we haven't already
- if (!store.#hasEmittedMaxSizeEvent) {
- store.emit('maxSizeExceeded', {
- size: store.#size,
- maxSize: store.#maxSize,
- count: store.#count,
- maxCount: store.#maxCount
- })
- store.#hasEmittedMaxSizeEvent = true
- }
- // Perform eviction
- for (const [key, entries] of store.#entries) {
- for (const entry of entries.splice(0, entries.length / 2)) {
- store.#size -= entry.size
- store.#count -= 1
- }
- if (entries.length === 0) {
- store.#entries.delete(key)
- }
- }
- // Reset the event flag after eviction
- if (store.#size < store.#maxSize && store.#count < store.#maxCount) {
- store.#hasEmittedMaxSizeEvent = false
- }
- }
- callback(null)
- }
- })
- }
- /**
- * @param {CacheKey} key
- */
- delete (key) {
- if (typeof key !== 'object') {
- throw new TypeError(`expected key to be object, got ${typeof key}`)
- }
- const topLevelKey = `${key.origin}:${key.path}`
- for (const entry of this.#entries.get(topLevelKey) ?? []) {
- this.#size -= entry.size
- this.#count -= 1
- }
- this.#entries.delete(topLevelKey)
- }
- }
- function findEntry (key, entries, now) {
- return entries.find((entry) => (
- entry.deleteAt > now &&
- entry.method === key.method &&
- (entry.vary == null || Object.keys(entry.vary).every(headerName => {
- if (entry.vary[headerName] === null) {
- return key.headers[headerName] === undefined
- }
- return entry.vary[headerName] === key.headers[headerName]
- }))
- ))
- }
- module.exports = MemoryCacheStore
|