'use strict'; const { Writable } = require('node:stream'); 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} */ class MemoryCacheStore { #maxCount = Infinity; #maxSize = Infinity; #maxEntrySize = Infinity; #size = 0; #count = 0; #entries = new Map(); /** * @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts] */ constructor(opts) { 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; } } } /** * @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 entry = this.#entries.get(topLevelKey)?.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]; })) ); 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); } entries.push(entry); store.#size += entry.size; store.#count += 1; if (store.#size > store.#maxSize || store.#count > store.#maxCount) { 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); } } } 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); } } module.exports = MemoryCacheStore;