131 lines
3.7 KiB
JavaScript
131 lines
3.7 KiB
JavaScript
import fetch from 'node-fetch';
|
|
import fs from 'fs';
|
|
import { URLSearchParams } from 'url';
|
|
import crypto from 'crypto';
|
|
import locko from 'locko';
|
|
import { NFCResponse } from './classes/response.js';
|
|
import { MemoryCache } from './classes/caching/memory_cache.js';
|
|
|
|
const CACHE_VERSION = 3;
|
|
|
|
function md5(str) {
|
|
return crypto.createHash('md5').update(str).digest('hex');
|
|
}
|
|
|
|
// Since the bounday in FormData is random,
|
|
// we ignore it for purposes of calculating
|
|
// the cache key.
|
|
function getFormDataCacheKey(formData) {
|
|
const cacheKey = { ...formData };
|
|
const boundary = formData.getBoundary();
|
|
|
|
// eslint-disable-next-line no-underscore-dangle
|
|
delete cacheKey._boundary;
|
|
|
|
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;
|
|
}
|
|
|
|
function getBodyCacheKeyJson(body) {
|
|
if (!body) {
|
|
return body;
|
|
} if (typeof body === 'string') {
|
|
return body;
|
|
} if (body instanceof URLSearchParams) {
|
|
return body.toString();
|
|
} if (body instanceof fs.ReadStream) {
|
|
return body.path;
|
|
} if (body.toString && body.toString() === '[object FormData]') {
|
|
return getFormDataCacheKey(body);
|
|
}
|
|
|
|
throw new Error('Unsupported body type. Supported body types are: string, number, undefined, null, url.URLSearchParams, fs.ReadStream, FormData');
|
|
}
|
|
|
|
function getCacheKey(requestArguments) {
|
|
const resource = requestArguments[0];
|
|
const init = requestArguments[1] || {};
|
|
|
|
if (typeof resource !== 'string') {
|
|
throw new Error('The first argument must be a string (fetch.Request is not supported).');
|
|
}
|
|
|
|
const resourceCacheKeyJson = { url: resource };
|
|
const initCacheKeyJson = { ...init };
|
|
|
|
resourceCacheKeyJson.body = getBodyCacheKeyJson(resourceCacheKeyJson.body);
|
|
initCacheKeyJson.body = getBodyCacheKeyJson(initCacheKeyJson.body);
|
|
|
|
delete resourceCacheKeyJson.agent;
|
|
delete initCacheKeyJson.agent;
|
|
|
|
return md5(JSON.stringify([resourceCacheKeyJson, initCacheKeyJson, CACHE_VERSION]));
|
|
}
|
|
|
|
async function getResponse(cache, requestArguments) {
|
|
const cacheKey = getCacheKey(requestArguments);
|
|
let cachedValue = await cache.get(cacheKey);
|
|
|
|
const ejectSelfFromCache = () => cache.remove(cacheKey);
|
|
|
|
if (cachedValue) {
|
|
return NFCResponse.fromCachedResponse(
|
|
cachedValue.bodyStream,
|
|
cachedValue.metaData,
|
|
ejectSelfFromCache,
|
|
);
|
|
}
|
|
|
|
await locko.lock(cacheKey);
|
|
try {
|
|
cachedValue = await cache.get(cacheKey);
|
|
if (cachedValue) {
|
|
return NFCResponse.fromCachedResponse(
|
|
cachedValue.bodyStream,
|
|
cachedValue.metaData,
|
|
ejectSelfFromCache,
|
|
);
|
|
}
|
|
|
|
const fetchResponse = await fetch(...requestArguments);
|
|
const nfcResponse = NFCResponse.fromNodeFetchResponse(fetchResponse, ejectSelfFromCache);
|
|
const contentLength = Number.parseInt(nfcResponse.headers.get('content-length'), 10) || 0;
|
|
const nfcResponseSerialized = nfcResponse.serialize();
|
|
|
|
await cache.set(
|
|
cacheKey,
|
|
nfcResponseSerialized.bodyStream,
|
|
nfcResponseSerialized.metaData,
|
|
contentLength,
|
|
);
|
|
|
|
return nfcResponse;
|
|
} finally {
|
|
locko.unlock(cacheKey);
|
|
}
|
|
}
|
|
|
|
function createFetchWithCache(cache) {
|
|
const fetchCache = (...args) => getResponse(cache, args);
|
|
fetchCache.withCache = createFetchWithCache;
|
|
|
|
return fetchCache;
|
|
}
|
|
|
|
const defaultFetch = createFetchWithCache(new MemoryCache());
|
|
|
|
export default defaultFetch;
|
|
export const fetchBuilder = defaultFetch;
|
|
export { MemoryCache } from './classes/caching/memory_cache.js';
|
|
export { FileSystemCache } from './classes/caching/file_system_cache.js';
|