diff --git a/index.js b/index.js index ecff4a1..903e2c0 100644 --- a/index.js +++ b/index.js @@ -7,10 +7,48 @@ function md5(str) { return crypto.createHash('md5').update(str).digest('hex'); } +class Headers { + constructor(rawHeaders) { + this.rawHeaders = rawHeaders; + } + + * entries() { + for (let entry of Object.entries(this.rawHeaders)) { + yield entry; + } + } + + * keys() { + for (let key of Object.keys(this.rawHeaders)) { + yield key; + } + } + + * values() { + for (let value of Object.values(this.rawHeaders)) { + yield value; + } + } + + get(name) { + return this.rawHeaders[name.toLowerCase()] || null; + } + + has(name) { + return !!this.get(name); + } +} + class Response { - constructor(raw, cacheFilePath) { + constructor(raw, cacheFilePath, fromCache) { Object.assign(this, raw); this.cacheFilePath = cacheFilePath; + this.headers = new Headers(raw.headers); + this.fromCache = fromCache; + + if (this.bodyBuffer.type === 'Buffer') { + this.bodyBuffer = Buffer.from(this.bodyBuffer); + } } text() { @@ -33,14 +71,16 @@ class Response { async function createRawResponse(fetchRes) { const buffer = await fetchRes.buffer(); + const rawHeaders = Array.from(fetchRes.headers.entries()) + .reduce((aggregate, entry) => ({ ...aggregate, [entry[0]]: entry[1] }), {}); + return { status: fetchRes.status, statusText: fetchRes.statusText, type: fetchRes.type, url: fetchRes.url, - useFinalURL: fetchRes.useFinalURL, ok: fetchRes.ok, - headers: fetchRes.headers, + headers: rawHeaders, redirected: fetchRes.redirected, bodyBuffer: buffer, }; @@ -52,17 +92,17 @@ async function getResponse(cacheDirPath, requestArguments) { ? ({ ...requestInit, body: typeof requestInit.body === 'object' ? requestInit.body.toString() : requestInit.body }) : requestInit; - const cacheHash = md5(JSON.stringify([url, requestParams, ...rest]) + bodyFunctionName); + const cacheHash = md5(JSON.stringify([url, requestParams, ...rest])); const cachedFilePath = path.join(cacheDirPath, `${cacheHash}.json`); try { const rawResponse = JSON.parse(await fs.promises.readFile(cachedFilePath)); - return new Response(rawResponse); + return new Response(rawResponse, cachedFilePath, true); } catch (err) { const fetchResponse = await fetch(...requestArguments); - const rawResponse = createRawResponse(fetchResponse); + const rawResponse = await createRawResponse(fetchResponse); await fs.promises.writeFile(cachedFilePath, JSON.stringify(rawResponse)); - return new Response(rawResponse); + return new Response(rawResponse, cachedFilePath, false); } } diff --git a/package-lock.json b/package-lock.json index 3225c50..78c43a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -750,6 +750,17 @@ "flatted": "^2.0.0", "rimraf": "2.6.3", "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "flatted": { @@ -1730,9 +1741,9 @@ } }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" diff --git a/package.json b/package.json index 8bbefba..f39b21c 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "eslint": "^6.8.0", "eslint-config-airbnb-base": "^14.1.0", "eslint-plugin-import": "^2.20.2", - "mocha": "^8.2.1" + "mocha": "^8.2.1", + "rimraf": "^3.0.2" }, "dependencies": { "node-fetch": "*" diff --git a/test/tests.js b/test/tests.js index 074e816..588bf62 100644 --- a/test/tests.js +++ b/test/tests.js @@ -1,50 +1,95 @@ const assert = require('assert'); +const rimraf = require('rimraf'); const path = require('path'); -const fetch = require('../index.js')(path.join(__dirname, '..', '.cache')); +const FetchCache = require('../index.js'); + +const CACHE_PATH = path.join(__dirname, '..', '.cache'); const TWO_HUNDRED_URL = 'https://httpbin.org/status/200'; const FOUR_HUNDRED_URL = 'https://httpbin.org/status/400'; const THREE_HUNDRED_TWO_URL = 'https://httpbin.org/status/302'; +const TEXT_BODY_URL = 'https://httpbin.org/robots.txt'; +const TEXT_BODY_EXPECTED = 'User-agent: *\nDisallow: /deny\n'; + +let fetch; +let res; +let body; + +beforeEach(async function() { + rimraf.sync(CACHE_PATH); + fetch = FetchCache(CACHE_PATH); +}); describe('Basic property tests', function() { it('Has a status property', async function() { - const res = await fetch(TWO_HUNDRED_URL); + res = await fetch(TWO_HUNDRED_URL); + assert.strictEqual(res.status, 200); + + res = await fetch(TWO_HUNDRED_URL); assert.strictEqual(res.status, 200); }); it('Has a statusText property', async function() { - const res = await fetch(TWO_HUNDRED_URL); + res = await fetch(TWO_HUNDRED_URL); + assert.strictEqual(res.statusText, 'OK'); + + res = await fetch(TWO_HUNDRED_URL); assert.strictEqual(res.statusText, 'OK'); }); - it('Has a type property', async function() { - const res = await fetch(TWO_HUNDRED_URL); - assert.strictEqual(res.type, 'basic'); - }); - it('Has a url property', async function() { - const res = await fetch(TWO_HUNDRED_URL); + res = await fetch(TWO_HUNDRED_URL); + assert.strictEqual(res.url, TWO_HUNDRED_URL); + + res = await fetch(TWO_HUNDRED_URL); assert.strictEqual(res.url, TWO_HUNDRED_URL); }); - it('Has a useFinalURL property', async function() { - const res = await fetch(TWO_HUNDRED_URL); - assert.strictEqual(res.useFinalURL, true); - }); - it('Has an ok property', async function() { - const res = await fetch(FOUR_HUNDRED_URL); + res = await fetch(FOUR_HUNDRED_URL); + assert.strictEqual(res.ok, false); + assert.strictEqual(res.status, 400); + + res = await fetch(FOUR_HUNDRED_URL); assert.strictEqual(res.ok, false); assert.strictEqual(res.status, 400); }); it('Has a headers property', async function() { - const res = await fetch(TWO_HUNDRED_URL); + res = await fetch(TWO_HUNDRED_URL); + assert.notStrictEqual(res.headers, undefined); + + res = await fetch(TWO_HUNDRED_URL); assert.notStrictEqual(res.headers, undefined); }); it('Has a redirected property', async function() { - const res = await fetch(THREE_HUNDRED_TWO_URL); + res = await fetch(THREE_HUNDRED_TWO_URL); + assert.strictEqual(res.redirected, true); + + res = await fetch(THREE_HUNDRED_TWO_URL); assert.strictEqual(res.redirected, true); }); -}); \ No newline at end of file +}).timeout(10000); + +describe('Cache tests', function() { + it('Uses cache', async function() { + res = await fetch(TWO_HUNDRED_URL); + assert.strictEqual(res.fromCache, false); + + res = await fetch(TWO_HUNDRED_URL); + assert.strictEqual(res.fromCache, true); + }); +}).timeout(10000); + +describe('Data tests', function() { + it('Can get text body', async function() { + res = await fetch(TEXT_BODY_URL); + body = await res.text(); + assert.strictEqual(body, TEXT_BODY_EXPECTED); + + res = await fetch(TEXT_BODY_URL); + body = await res.text(); + assert.strictEqual(body, TEXT_BODY_EXPECTED); + }); +}).timeout(10000);