@graphql-mesh/plugin-response-cache
Version:
212 lines (211 loc) • 9.25 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = useMeshResponseCache;
const tslib_1 = require("tslib");
const cache_control_parser_1 = tslib_1.__importDefault(require("cache-control-parser"));
const response_cache_1 = require("@envelop/response-cache");
const cross_helpers_1 = require("@graphql-mesh/cross-helpers");
const string_interpolation_1 = require("@graphql-mesh/string-interpolation");
const utils_1 = require("@graphql-tools/utils");
const plugin_response_cache_1 = require("@graphql-yoga/plugin-response-cache");
const promise_helpers_1 = require("@whatwg-node/promise-helpers");
function generateSessionIdFactory(sessionIdDef) {
if (sessionIdDef == null) {
return function voidSession() {
return null;
};
}
return function session(context) {
return string_interpolation_1.stringInterpolator.parse(sessionIdDef, {
context,
env: cross_helpers_1.process.env,
});
};
}
function generateEnabledFactory(ifDef) {
return function enabled(context) {
// eslint-disable-next-line no-new-func
return new Function('context', `return ${ifDef}`)(context);
};
}
function getBuildResponseCacheKey(cacheKeyDef) {
return function buildResponseCacheKey(cacheKeyParameters) {
let cacheKey = string_interpolation_1.stringInterpolator.parse(cacheKeyDef, {
...cacheKeyParameters,
env: cross_helpers_1.process.env,
});
if (!cacheKey) {
cacheKey = (0, response_cache_1.defaultBuildResponseCacheKey)(cacheKeyParameters);
}
return cacheKey;
};
}
function getShouldCacheResult(shouldCacheResultDef) {
return function shouldCacheResult({ result }) {
// eslint-disable-next-line no-new-func
return new Function(`return ${shouldCacheResultDef}`)();
};
}
function getCacheForResponseCache(meshCache) {
if (!meshCache) {
throw new Error('Response Cache is enabled, but no cache is configured. Please provide a cache instance through the `cache` option.');
}
return {
get(responseId) {
return meshCache.get(`response-cache:${responseId}`);
},
set(responseId, data, entities, ttl) {
const ttlConfig = Number.isFinite(ttl) ? { ttl: ttl / 1000 } : undefined;
const jobs = [];
const job = meshCache.set(`response-cache:${responseId}`, data, ttlConfig);
if ((0, utils_1.isPromise)(job)) {
jobs.push(job);
}
for (const { typename, id } of entities) {
const entryId = `${typename}.${id}`;
const job1 = meshCache.set(`response-cache:${entryId}:${responseId}`, {}, ttlConfig);
const job2 = meshCache.set(`response-cache:${responseId}:${entryId}`, {}, ttlConfig);
if ((0, utils_1.isPromise)(job1)) {
jobs.push(job1);
}
if ((0, utils_1.isPromise)(job2)) {
jobs.push(job2);
}
}
if (jobs.length === 0) {
return undefined;
}
if (jobs.length === 1) {
return jobs[0];
}
return Promise.all(jobs).then(() => undefined);
},
invalidate(entitiesToRemove) {
const responseIdsToCheck = new Set();
const entitiesToRemoveJobs = [];
for (const { typename, id } of entitiesToRemove) {
const entryId = `${typename}.${id}`;
const job = (0, promise_helpers_1.handleMaybePromise)(() => meshCache.getKeysByPrefix(`response-cache:${entryId}:`), cacheEntriesToDelete => {
const jobs = [];
for (const cacheEntryName of cacheEntriesToDelete) {
const [, , responseId] = cacheEntryName.split(':');
responseIdsToCheck.add(responseId);
const job = meshCache.delete(cacheEntryName);
if ((0, utils_1.isPromise)(job)) {
jobs.push(job);
}
}
if (jobs.length === 0) {
return undefined;
}
if (jobs.length === 1) {
return jobs[0];
}
return Promise.all(jobs);
});
if ((0, utils_1.isPromise)(job)) {
entitiesToRemoveJobs.push(job);
}
}
let promiseAllJob;
if (entitiesToRemoveJobs.length === 1) {
promiseAllJob = entitiesToRemoveJobs[0];
}
else if (entitiesToRemoveJobs.length > 0) {
promiseAllJob = Promise.all(entitiesToRemoveJobs);
}
return (0, promise_helpers_1.handleMaybePromise)(() => promiseAllJob, () => {
const responseIdsToCheckJobs = [];
for (const responseId of responseIdsToCheck) {
const job = (0, promise_helpers_1.handleMaybePromise)(() => meshCache.getKeysByPrefix(`response-cache:${responseId}:`), cacheEntries => {
if (cacheEntries.length !== 0) {
return meshCache.delete(`response-cache:${responseId}`);
}
return undefined;
});
if ((0, utils_1.isPromise)(job)) {
responseIdsToCheckJobs.push(job);
}
}
if (responseIdsToCheckJobs.length === 0) {
return undefined;
}
else if (responseIdsToCheckJobs.length === 1) {
return responseIdsToCheckJobs[0];
}
return Promise.all(responseIdsToCheckJobs);
});
},
};
}
function useMeshResponseCache(options) {
const ttlPerType = { ...options.ttlPerType };
const ttlPerSchemaCoordinate = {
...options.ttlPerSchemaCoordinate,
};
const { ttlPerCoordinate } = options;
if (ttlPerCoordinate) {
for (const ttlConfig of ttlPerCoordinate) {
ttlPerSchemaCoordinate[ttlConfig.coordinate] = ttlConfig.ttl;
}
}
// Stored TTL by the context
// To be compared with the calculated one later in `onTtl`
const ttlByContext = new WeakMap();
// @ts-expect-error - GatewayPlugin types
const plugin = (0, plugin_response_cache_1.useResponseCache)({
includeExtensionMetadata: options.includeExtensionMetadata != null
? options.includeExtensionMetadata
: cross_helpers_1.process.env.DEBUG === '1',
session: 'sessionId' in options ? generateSessionIdFactory(options.sessionId) : () => null,
enabled: 'if' in options ? generateEnabledFactory(options.if) : undefined,
buildResponseCacheKey: 'cacheKey' in options ? getBuildResponseCacheKey(options.cacheKey) : undefined,
...options,
shouldCacheResult: typeof options.shouldCacheResult === 'string'
? getShouldCacheResult(options.shouldCacheResult)
: options.shouldCacheResult,
cache: getCacheForResponseCache(options.cache),
ttlPerType,
ttlPerSchemaCoordinate,
// Checks the TTL stored in the context
// Compares it to the calculated one
// Then it takes the lowest value
onTtl({ ttl, context }) {
const ttlForThisContext = ttlByContext.get(context);
if (ttlForThisContext != null && ttlForThisContext < ttl) {
return ttlForThisContext;
}
return ttl;
},
});
// Checks the TTL stored in the context
// Takes the lowest value
function checkTtl(context, ttl) {
const ttlForThisContext = ttlByContext.get(context);
if (ttlForThisContext == null || ttl < ttlForThisContext) {
ttlByContext.set(context, ttl);
}
}
plugin.onFetch = function ({ executionRequest, context }) {
// Only if it is a subgraph request
if (executionRequest && context) {
return function onFetchDone({ response }) {
const cacheControlHeader = response.headers.get('cache-control');
if (cacheControlHeader != null) {
const parsedCacheControl = cache_control_parser_1.default.parse(cacheControlHeader);
if (parsedCacheControl['max-age'] != null) {
const maxAgeInSeconds = parsedCacheControl['max-age'];
const maxAgeInMs = maxAgeInSeconds * 1000;
checkTtl(context, maxAgeInMs);
}
if (parsedCacheControl['s-maxage'] != null) {
const sMaxAgeInSeconds = parsedCacheControl['s-maxage'];
const sMaxAgeInMs = sMaxAgeInSeconds * 1000;
checkTtl(context, sMaxAgeInMs);
}
}
};
}
};
return plugin;
}
;