UNPKG

@vtex/api

Version:
221 lines (220 loc) • 11.8 kB
"use strict"; 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 }); } };