diff --git a/README.md b/README.md index f03c01a..31e3883 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ const fetchBuilder, { MemoryCache } = require('node-fetch-cache'); const fetch = fetchBuilder.withCache(new MemoryCache(options)); ``` -Supported options: +Options: ```js { @@ -140,6 +140,7 @@ const fetch = fetchBuilder.withCache(new FileSystemCache(options)); ```js { + cacheDirectory: '/my/cache/directory/path', // Specify where to keep the cache. If undefined, '.cache' is used by default. If this directory does not exist, it will be created. ttl: 1000, // Time to live. How long (in ms) responses remain cached before being automatically ejected. If undefined, responses are never automatically ejected from the cache. } ``` diff --git a/classes/caching/file_system_cache.js b/classes/caching/file_system_cache.js new file mode 100644 index 0000000..074c2d1 --- /dev/null +++ b/classes/caching/file_system_cache.js @@ -0,0 +1,29 @@ +const FPersist = require('fpersist'); +const KeyTimeout = require('./key_timeout.js'); + +module.exports = class FileSystemCache { + constructor(options = {}) { + this.ttl = options.ttl; + this.keyTimeout = new KeyTimeout(); + + const cacheDirectory = options.cacheDirectory || '.cache'; + this.cache = new FPersist(cacheDirectory); + } + + get(key) { + return this.cache.getItem(key); + } + + remove(key) { + this.keyTimeout.clearTimeout(key); + return this.cache.deleteItem(key); + } + + async set(key, value) { + await this.cache.setItem(key, value); + + if (typeof this.ttl === 'number') { + this.keyTimeout.updateTimeout(key, this.ttl, () => this.remove(key)); + } + } +} diff --git a/classes/caching/key_timeout.js b/classes/caching/key_timeout.js new file mode 100644 index 0000000..635c117 --- /dev/null +++ b/classes/caching/key_timeout.js @@ -0,0 +1,16 @@ +module.exports = class KeyTimeout { + constructor() { + this.timeoutHandleForKey = {}; + } + + clearTimeout(key) { + clearTimeout(this.timeoutHandleForKey[key]); + } + + updateTimeout(key, durationMs, callback) { + this.clearTimeout(key); + this.timeoutHandleForKey[key] = setTimeout(() => { + callback(); + }, durationMs); + } +} \ No newline at end of file diff --git a/classes/caching/memory_cache.js b/classes/caching/memory_cache.js new file mode 100644 index 0000000..9428f5d --- /dev/null +++ b/classes/caching/memory_cache.js @@ -0,0 +1,33 @@ +const KeyTimeout = require('./key_timeout.js'); + +module.exports = class MemoryCache { + constructor(options = {}) { + this.ttl = options.ttl; + this.keyTimeout = new KeyTimeout(); + + if (options.global && !globalThis.nodeFetchCache) { + globalThis.nodeFetchCache = {}; + } + + this.cache = options.global + ? globalThis.nodeFetchCache + : {}; + } + + get(key) { + return this.cache[key]; + } + + remove(key) { + this.keyTimeout.clearTimeout(key); + delete this.cache[key]; + } + + set(key, value) { + this.cache[key] = value; + + if (typeof this.ttl === 'number') { + this.keyTimeout.updateTimeout(key, this.ttl, () => this.remove(key)); + } + } +} diff --git a/classes/response.js b/classes/response.js index b22443f..2f2c3e4 100644 --- a/classes/response.js +++ b/classes/response.js @@ -3,9 +3,9 @@ const stream = require('stream'); const Headers = require('./headers.js'); class Response { - constructor(raw, cacheFilePath, fromCache) { + constructor(raw, ejectSelfFromCache, fromCache) { Object.assign(this, raw); - this.cacheFilePath = cacheFilePath; + this.ejectSelfFromCache = ejectSelfFromCache; this.headers = new Headers(raw.headers); this.fromCache = fromCache; this.bodyUsed = false; @@ -40,14 +40,8 @@ class Response { return this.consumeBody(); } - async ejectFromCache() { - try { - await fs.promises.unlink(this.cacheFilePath); - } catch (err) { - if (err.code !== 'ENOENT') { - throw err; - } - } + ejectFromCache() { + return this.ejectSelfFromCache(); } } diff --git a/index.js b/index.js index 6927106..2787119 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const { URLSearchParams } = require('url'); const crypto = require('crypto'); const path = require('path'); const Response = require('./classes/response.js'); +const MemoryCache = require('./classes/caching/memory_cache.js'); const CACHE_VERSION = 2; @@ -88,32 +89,27 @@ async function createRawResponse(fetchRes) { }; } -async function getResponse(cacheDirPath, requestArguments) { +async function getResponse(cache, requestArguments) { const cacheKey = getCacheKey(requestArguments); - const cachedFilePath = path.join(cacheDirPath, `${cacheKey}.json`); + const cachedValue = await cache.get(cacheKey); - try { - const rawResponse = JSON.parse(await fs.promises.readFile(cachedFilePath)); - return new Response(rawResponse, cachedFilePath, true); - } catch (err) { + const ejectSelfFromCache = () => cache.remove(cacheKey); + + if (cachedValue) { + return new Response(cachedValue, ejectSelfFromCache, true); + } else { const fetchResponse = await fetch(...requestArguments); const rawResponse = await createRawResponse(fetchResponse); - await fs.promises.writeFile(cachedFilePath, JSON.stringify(rawResponse)); - return new Response(rawResponse, cachedFilePath, false); + await cache.set(cacheKey, rawResponse); + return new Response(rawResponse, ejectSelfFromCache, false); } } -function createFetch(cacheDirPath) { - let madeDir = false; +function createFetchWithCache(cache) { + const fetch = (...args) => getResponse(cache, args); + fetch.withCache = createFetchWithCache; - return async (...args) => { - if (!madeDir) { - await fs.promises.mkdir(cacheDirPath, { recursive: true }); - madeDir = true; - } - - return getResponse(cacheDirPath, args); - }; + return fetch; } -module.exports = createFetch; +module.exports = createFetchWithCache(new MemoryCache()); diff --git a/test/expected_png.png b/test/expected_png.png new file mode 100644 index 0000000..cc0c128 Binary files /dev/null and b/test/expected_png.png differ diff --git a/test/tests.js b/test/tests.js index b1df47a..5223218 100644 --- a/test/tests.js +++ b/test/tests.js @@ -5,6 +5,7 @@ const rimraf = require('rimraf'); const path = require('path'); const FetchCache = require('../index.js'); const { URLSearchParams } = require('url'); +const MemoryCache = require('../classes/caching/memory_cache.js'); const CACHE_PATH = path.join(__dirname, '..', '.cache'); const expectedPngBuffer = fs.readFileSync(path.join(__dirname, 'expected_png.png')); @@ -28,7 +29,7 @@ function post(body) { beforeEach(async function() { rimraf.sync(CACHE_PATH); - fetch = FetchCache(CACHE_PATH); + fetch = FetchCache.withCache(new MemoryCache()); }); describe('Basic property tests', function() {