2020-04-17 20:31:22 +00:00
|
|
|
const fetch = require('node-fetch');
|
|
|
|
const fs = require('fs');
|
2020-11-28 16:17:25 +00:00
|
|
|
const { URLSearchParams } = require('url');
|
2020-04-17 20:31:22 +00:00
|
|
|
const crypto = require('crypto');
|
|
|
|
const path = require('path');
|
2020-11-28 04:28:00 +00:00
|
|
|
const Response = require('./classes/response.js');
|
|
|
|
|
2020-11-28 16:45:51 +00:00
|
|
|
const CACHE_VERSION = 2;
|
|
|
|
|
2020-04-17 20:31:22 +00:00
|
|
|
function md5(str) {
|
|
|
|
return crypto.createHash('md5').update(str).digest('hex');
|
|
|
|
}
|
|
|
|
|
2021-06-10 15:11:33 +00:00
|
|
|
// Since the bounday in FormData is random,
|
|
|
|
// we ignore it for purposes of calculating
|
|
|
|
// the cache key.
|
2020-11-28 16:43:50 +00:00
|
|
|
function getFormDataCacheKey(formData) {
|
|
|
|
const cacheKey = { ...formData };
|
|
|
|
|
|
|
|
if (typeof formData.getBoundary === 'function') {
|
|
|
|
const boundary = formData.getBoundary();
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
|
|
delete cacheKey._boundary;
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
|
|
if (Array.isArray(cacheKey._streams)) {
|
|
|
|
const boundaryReplaceRegex = new RegExp(boundary, 'g');
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
|
|
cacheKey._streams = cacheKey._streams.map((s) => {
|
|
|
|
if (typeof s === 'string') {
|
|
|
|
return s.replace(boundaryReplaceRegex, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
return s;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cacheKey;
|
|
|
|
}
|
|
|
|
|
2020-11-28 16:17:25 +00:00
|
|
|
function getBodyCacheKeyJson(body) {
|
2020-11-28 16:25:23 +00:00
|
|
|
if (!body) {
|
|
|
|
return body;
|
|
|
|
} if (typeof body === 'string') {
|
2020-11-28 16:17:25 +00:00
|
|
|
return body;
|
|
|
|
} if (body instanceof URLSearchParams) {
|
|
|
|
return body.toString();
|
2020-11-28 16:25:23 +00:00
|
|
|
} if (body instanceof fs.ReadStream) {
|
|
|
|
return body.path;
|
2020-11-28 16:43:50 +00:00
|
|
|
} if (body.toString && body.toString() === '[object FormData]') {
|
|
|
|
return getFormDataCacheKey(body);
|
2020-11-28 16:17:25 +00:00
|
|
|
}
|
|
|
|
|
2020-11-28 16:25:23 +00:00
|
|
|
throw new Error('Unsupported body type');
|
2020-11-28 16:17:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function getCacheKey(requestArguments) {
|
|
|
|
const resource = requestArguments[0];
|
|
|
|
const init = requestArguments[1] || {};
|
|
|
|
|
|
|
|
const resourceCacheKeyJson = typeof resource === 'string' ? { url: resource } : { ...resource };
|
|
|
|
const initCacheKeyJson = { ...init };
|
|
|
|
|
|
|
|
resourceCacheKeyJson.body = getBodyCacheKeyJson(resourceCacheKeyJson.body);
|
|
|
|
initCacheKeyJson.body = getBodyCacheKeyJson(initCacheKeyJson.body);
|
|
|
|
|
2020-11-28 16:45:51 +00:00
|
|
|
return md5(JSON.stringify([resourceCacheKeyJson, initCacheKeyJson, CACHE_VERSION]));
|
2020-11-28 16:17:25 +00:00
|
|
|
}
|
|
|
|
|
2020-11-28 02:46:49 +00:00
|
|
|
async function createRawResponse(fetchRes) {
|
|
|
|
const buffer = await fetchRes.buffer();
|
|
|
|
|
2020-11-28 03:26:31 +00:00
|
|
|
const rawHeaders = Array.from(fetchRes.headers.entries())
|
|
|
|
.reduce((aggregate, entry) => ({ ...aggregate, [entry[0]]: entry[1] }), {});
|
|
|
|
|
2020-11-28 02:46:49 +00:00
|
|
|
return {
|
|
|
|
status: fetchRes.status,
|
|
|
|
statusText: fetchRes.statusText,
|
|
|
|
type: fetchRes.type,
|
|
|
|
url: fetchRes.url,
|
|
|
|
ok: fetchRes.ok,
|
2020-11-28 03:26:31 +00:00
|
|
|
headers: rawHeaders,
|
2020-11-28 02:46:49 +00:00
|
|
|
redirected: fetchRes.redirected,
|
|
|
|
bodyBuffer: buffer,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getResponse(cacheDirPath, requestArguments) {
|
2020-11-28 16:17:25 +00:00
|
|
|
const cacheKey = getCacheKey(requestArguments);
|
|
|
|
const cachedFilePath = path.join(cacheDirPath, `${cacheKey}.json`);
|
2020-11-28 02:46:49 +00:00
|
|
|
|
|
|
|
try {
|
|
|
|
const rawResponse = JSON.parse(await fs.promises.readFile(cachedFilePath));
|
2020-11-28 03:26:31 +00:00
|
|
|
return new Response(rawResponse, cachedFilePath, true);
|
2020-11-28 02:46:49 +00:00
|
|
|
} catch (err) {
|
|
|
|
const fetchResponse = await fetch(...requestArguments);
|
2020-11-28 03:26:31 +00:00
|
|
|
const rawResponse = await createRawResponse(fetchResponse);
|
2020-11-28 02:46:49 +00:00
|
|
|
await fs.promises.writeFile(cachedFilePath, JSON.stringify(rawResponse));
|
2020-11-28 03:26:31 +00:00
|
|
|
return new Response(rawResponse, cachedFilePath, false);
|
2020-04-17 20:31:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function createFetch(cacheDirPath) {
|
|
|
|
let madeDir = false;
|
|
|
|
|
|
|
|
return async (...args) => {
|
|
|
|
if (!madeDir) {
|
2020-11-28 02:46:49 +00:00
|
|
|
await fs.promises.mkdir(cacheDirPath, { recursive: true });
|
2020-04-17 20:31:22 +00:00
|
|
|
madeDir = true;
|
|
|
|
}
|
|
|
|
|
2020-11-28 02:46:49 +00:00
|
|
|
return getResponse(cacheDirPath, args);
|
2020-04-17 20:31:22 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = createFetch;
|