@vtex/api
Version:
VTEX I/O API client
221 lines (220 loc) • 11.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.cacheMiddleware = exports.CacheType = exports.isLocallyCacheable = exports.cacheKey = void 0;
const constants_1 = require("../../constants");
const tracing_1 = require("../../tracing");
const RANGE_HEADER_QS_KEY = '__range_header';
const cacheableStatusCodes = [200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501]; // https://tools.ietf.org/html/rfc7231#section-6.1
const cacheKey = (config) => {
const { baseURL = '', url = '', params, headers } = config;
const locale = headers === null || headers === void 0 ? void 0 : headers[constants_1.LOCALE_HEADER];
const encodedBaseURL = baseURL.replace(/\//g, '\\');
const encodedURL = url.replace(/\//g, '\\');
let key = `${locale}--${encodedBaseURL}--${encodedURL}?`;
if (params) {
Object.keys(params).sort().forEach(p => key = key.concat(`--${p}=${params[p]}`));
}
if (headers === null || headers === void 0 ? void 0 : headers.range) {
key = key.concat(`--${RANGE_HEADER_QS_KEY}=${headers.range}`);
}
return key;
};
exports.cacheKey = cacheKey;
const parseCacheHeaders = (headers) => {
const { 'cache-control': cacheControl = '', etag, age: ageStr } = headers;
const cacheDirectives = cacheControl.split(',').map(d => d.trim());
const maxAgeDirective = cacheDirectives.find(d => d.startsWith('max-age'));
const [, maxAgeStr] = maxAgeDirective ? maxAgeDirective.split('=') : [null, null];
const maxAge = maxAgeStr ? parseInt(maxAgeStr, 10) : 0;
const age = ageStr ? parseInt(ageStr, 10) : 0;
return {
age,
etag,
maxAge,
noCache: cacheDirectives.indexOf('no-cache') !== -1,
noStore: cacheDirectives.indexOf('no-store') !== -1,
};
};
function isLocallyCacheable(arg, type) {
return arg && !!arg.cacheable
&& (arg.cacheable === type || arg.cacheable === CacheType.Any || type === CacheType.Any);
}
exports.isLocallyCacheable = isLocallyCacheable;
const addNotModified = (validateStatus) => (status) => validateStatus(status) || status === 304;
var CacheType;
(function (CacheType) {
CacheType[CacheType["None"] = 0] = "None";
CacheType[CacheType["Memory"] = 1] = "Memory";
CacheType[CacheType["Disk"] = 2] = "Disk";
CacheType[CacheType["Any"] = 3] = "Any";
})(CacheType = exports.CacheType || (exports.CacheType = {}));
const CacheTypeNames = {
[CacheType.None]: 'none',
[CacheType.Memory]: 'memory',
[CacheType.Disk]: 'disk',
[CacheType.Any]: 'any',
};
const cacheMiddleware = ({ type, storage, asyncSet }) => {
const CACHE_RESULT_TAG = type === CacheType.Disk ? "http.cache.disk" /* CustomHttpTags.HTTP_DISK_CACHE_RESULT */ : "http.cache.memory" /* CustomHttpTags.HTTP_MEMORY_CACHE_RESULT */;
const cacheType = CacheTypeNames[type];
return async (ctx, next) => {
var _a, _b;
if (!isLocallyCacheable(ctx.config, type)) {
return await next();
}
const { rootSpan: span, tracer, logger } = (_a = ctx.tracing) !== null && _a !== void 0 ? _a : {};
const key = (0, exports.cacheKey)(ctx.config);
const segmentToken = (_b = ctx.config.headers) === null || _b === void 0 ? void 0 : _b[constants_1.SEGMENT_HEADER];
const keyWithSegment = key + segmentToken;
span === null || span === void 0 ? void 0 : span.log({
event: "cache-key-created" /* HttpLogEvents.CACHE_KEY_CREATE */,
["cache-type" /* HttpCacheLogFields.CACHE_TYPE */]: cacheType,
["key" /* HttpCacheLogFields.KEY */]: key,
["key-with-segment" /* HttpCacheLogFields.KEY_WITH_SEGMENT */]: keyWithSegment,
});
const cacheReadSpan = createCacheSpan(cacheType, 'read', tracer, span);
let cached = undefined;
try {
const cacheHasWithSegment = await storage.has(keyWithSegment);
cached = cacheHasWithSegment ? await storage.get(keyWithSegment) : await storage.get(key);
}
catch (error) {
tracing_1.ErrorReport.create({ originalError: error }).injectOnSpan(cacheReadSpan);
logger === null || logger === void 0 ? void 0 : logger.warn({ message: 'Error reading from the HttpClient cache', error });
}
finally {
cacheReadSpan === null || cacheReadSpan === void 0 ? void 0 : cacheReadSpan.finish();
}
if (cached && cached.response) {
const { etag: cachedEtag, response, expiration, responseType, responseEncoding } = cached;
if (type === CacheType.Disk && responseType === 'arraybuffer') {
response.data = Buffer.from(response.data, responseEncoding);
}
const now = Date.now();
span === null || span === void 0 ? void 0 : span.log({
event: "local-cache-hit-info" /* HttpLogEvents.LOCAL_CACHE_HIT_INFO */,
["cache-type" /* HttpCacheLogFields.CACHE_TYPE */]: cacheType,
["etag" /* HttpCacheLogFields.ETAG */]: cachedEtag,
["expiration-time" /* HttpCacheLogFields.EXPIRATION_TIME */]: (expiration - now) / 1000,
["response-type" /* HttpCacheLogFields.RESPONSE_TYPE */]: responseType,
["response-encoding" /* HttpCacheLogFields.RESPONSE_ENCONDING */]: responseEncoding,
});
if (expiration > now) {
ctx.response = response;
ctx.cacheHit = {
memory: 1,
revalidated: 0,
router: 0,
};
span === null || span === void 0 ? void 0 : span.setTag(CACHE_RESULT_TAG, "HIT" /* CacheResult.HIT */);
return;
}
span === null || span === void 0 ? void 0 : span.setTag(CACHE_RESULT_TAG, "STALE" /* CacheResult.STALE */);
const validateStatus = addNotModified(ctx.config.validateStatus);
if (cachedEtag && validateStatus(response.status)) {
ctx.config.headers = {
...ctx.config.headers,
'if-none-match': cachedEtag
};
ctx.config.validateStatus = validateStatus;
}
}
else {
span === null || span === void 0 ? void 0 : span.setTag(CACHE_RESULT_TAG, "MISS" /* CacheResult.MISS */);
}
await next();
if (!ctx.response) {
return;
}
const revalidated = ctx.response.status === 304;
if (revalidated && cached) {
ctx.response = cached.response;
ctx.cacheHit = {
memory: 1,
revalidated: 1,
router: 0,
};
}
const { data, headers, status } = ctx.response;
const { age, etag, maxAge: headerMaxAge, noStore, noCache } = parseCacheHeaders(headers);
const { forceMaxAge } = ctx.config;
const maxAge = forceMaxAge && cacheableStatusCodes.includes(status) ? Math.max(forceMaxAge, headerMaxAge) : headerMaxAge;
span === null || span === void 0 ? void 0 : span.log({
event: "cache-config" /* HttpLogEvents.CACHE_CONFIG */,
["cache-type" /* HttpCacheLogFields.CACHE_TYPE */]: cacheType,
["age" /* HttpCacheLogFields.AGE */]: age,
["calculated-max-age" /* HttpCacheLogFields.CALCULATED_MAX_AGE */]: maxAge,
["max-age" /* HttpCacheLogFields.MAX_AGE */]: headerMaxAge,
["force-max-age" /* HttpCacheLogFields.FORCE_MAX_AGE */]: forceMaxAge,
["etag" /* HttpCacheLogFields.ETAG */]: etag,
["no-cache" /* HttpCacheLogFields.NO_CACHE */]: noCache,
["no-store" /* HttpCacheLogFields.NO_STORE */]: noStore,
});
// Indicates this should NOT be cached and this request will not be considered a miss.
if (!forceMaxAge && (noStore || (noCache && !etag))) {
span === null || span === void 0 ? void 0 : span.log({ event: "no-local-cache-save" /* HttpLogEvents.NO_LOCAL_CACHE_SAVE */, ["cache-type" /* HttpCacheLogFields.CACHE_TYPE */]: cacheType });
return;
}
const shouldCache = maxAge || etag;
const varySession = ctx.response.headers.vary && ctx.response.headers.vary.includes(constants_1.SESSION_HEADER);
if (shouldCache && !varySession) {
const { responseType, responseEncoding: configResponseEncoding } = ctx.config;
const currentAge = revalidated ? 0 : age;
const varySegment = ctx.response.headers.vary && ctx.response.headers.vary.includes(constants_1.SEGMENT_HEADER);
const setKey = varySegment ? keyWithSegment : key;
const responseEncoding = configResponseEncoding || (responseType === 'arraybuffer' ? 'base64' : undefined);
const cacheableData = type === CacheType.Disk && responseType === 'arraybuffer'
? data.toString(responseEncoding)
: data;
const now = Date.now();
const expiration = now + (maxAge - currentAge) * 1000;
const alreadyExpired = expiration <= now;
const reusingRevalidatedCache = cached && (ctx.response === cached.response);
const shouldSkipCacheUpdate = alreadyExpired && reusingRevalidatedCache;
if (shouldSkipCacheUpdate) {
return;
}
const cacheWriteSpan = createCacheSpan(cacheType, 'write', tracer, span);
try {
const storageSet = () => storage.set(setKey, {
etag,
expiration,
response: { data: cacheableData, headers, status },
responseEncoding: responseEncoding,
responseType,
});
if (asyncSet) {
storageSet();
}
else {
await storageSet();
span === null || span === void 0 ? void 0 : span.log({
event: "local-cache-saved" /* HttpLogEvents.LOCAL_CACHE_SAVED */,
["cache-type" /* HttpCacheLogFields.CACHE_TYPE */]: cacheType,
["key-set" /* HttpCacheLogFields.KEY_SET */]: setKey,
["age" /* HttpCacheLogFields.AGE */]: currentAge,
["etag" /* HttpCacheLogFields.ETAG */]: etag,
["expiration-time" /* HttpCacheLogFields.EXPIRATION_TIME */]: (expiration - Date.now()) / 1000,
["response-encoding" /* HttpCacheLogFields.RESPONSE_ENCONDING */]: responseEncoding,
["response-type" /* HttpCacheLogFields.RESPONSE_TYPE */]: responseType,
});
}
}
catch (error) {
tracing_1.ErrorReport.create({ originalError: error }).injectOnSpan(cacheWriteSpan);
logger === null || logger === void 0 ? void 0 : logger.warn({ message: 'Error writing to the HttpClient cache', error });
}
finally {
cacheWriteSpan === null || cacheWriteSpan === void 0 ? void 0 : cacheWriteSpan.finish();
}
return;
}
span === null || span === void 0 ? void 0 : span.log({ event: "no-local-cache-save" /* HttpLogEvents.NO_LOCAL_CACHE_SAVE */, ["cache-type" /* HttpCacheLogFields.CACHE_TYPE */]: cacheType });
};
};
exports.cacheMiddleware = cacheMiddleware;
const createCacheSpan = (cacheType, operation, tracer, parentSpan) => {
if (tracer && tracer.isTraceSampled && cacheType === 'disk') {
return tracer.startSpan(`${operation}-disk-cache`, { childOf: parentSpan });
}
};