aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/node_modules/undici/lib/cache/memory-cache-store.js
diff options
context:
space:
mode:
Diffstat (limited to 'vanilla/node_modules/undici/lib/cache/memory-cache-store.js')
-rw-r--r--vanilla/node_modules/undici/lib/cache/memory-cache-store.js234
1 files changed, 234 insertions, 0 deletions
diff --git a/vanilla/node_modules/undici/lib/cache/memory-cache-store.js b/vanilla/node_modules/undici/lib/cache/memory-cache-store.js
new file mode 100644
index 0000000..dba29ae
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/cache/memory-cache-store.js
@@ -0,0 +1,234 @@
+'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