UNPKG

@next-boost/next-boost

Version:

Add a cache layer for next.js SSR pages. Use stale-while-revalidate to boost the performance.

133 lines (132 loc) 5.86 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const zlib_1 = require("zlib"); const cache_manager_1 = require("./cache-manager"); const metrics_1 = require("./metrics"); const payload_1 = require("./payload"); const renderer_1 = __importDefault(require("./renderer")); const utils_1 = require("./utils"); function matchRules(conf, req) { var _a, _b; const err = ['GET', 'HEAD'].indexOf((_a = req.method) !== null && _a !== void 0 ? _a : '') === -1; if (err) return { matched: false, ttl: -1 }; if (typeof conf.rules === 'function') { const ttl = conf.rules(req); if (ttl) return { matched: true, ttl }; } else { for (const rule of (_b = conf.rules) !== null && _b !== void 0 ? _b : []) { if (req.url && new RegExp(rule.regex).test(req.url)) { return { matched: true, ttl: rule.ttl }; } } } return { matched: false, ttl: 0 }; } /** * Wrap a http listener to serve cached response * * @param cache the cache * @param conf conf of next-boost * @param renderer the SSR renderer runs in worker thread * @param next pass-through handler * * @returns a request listener to use in http server */ const wrap = (cache, conf, renderer, next, metrics) => { return (req, res) => __awaiter(void 0, void 0, void 0, function* () { var _a; if (conf.metrics && (0, metrics_1.forMetrics)(req)) return (0, metrics_1.serveMetrics)(metrics, res); req.url = (0, utils_1.filterUrl)((_a = req.url) !== null && _a !== void 0 ? _a : '', conf.paramFilter); const key = conf.cacheKey ? conf.cacheKey(req) : req.url; const { matched, ttl } = matchRules(conf, req); if (!matched) { metrics.inc('bypass'); res.setHeader('x-next-boost-status', 'bypass'); return next(req, res); } const start = process.hrtime(); const forced = req.headers['x-next-boost'] === 'update'; // forced const state = yield (0, cache_manager_1.serveCache)(cache, key, forced); res.setHeader('x-next-boost-status', state.status); metrics.inc(state.status); if (state.status === 'stale' || state.status === 'hit' || state.status === 'fulfill') { (0, cache_manager_1.send)(state.payload, res); if (!conf.quiet) (0, utils_1.log)(start, state.status, req.url); // record time for stale and hit if (state.status !== 'stale') return; // stop here } else if (state.status === 'timeout') { (0, cache_manager_1.send)({ body: null, headers: null }, res); return; // prevent adding pressure to server } try { yield (0, cache_manager_1.lock)(key, cache); const args = { path: req.url, headers: req.headers, method: req.method }; const rv = yield renderer.render(args); // rv.body is a Buffer in JSON format: { type: 'Buffer', data: [...] } const body = Buffer.from(rv.body); // stale has been served if (state.status !== 'stale') (0, utils_1.serve)(res, rv); // when in stale, there will 2 log output. The latter is the rendering time on server if (!conf.quiet) (0, utils_1.log)(start, state.status, req.url); if (rv.statusCode === 200) { // save gzipped data const payload = { headers: rv.headers, body: (0, utils_1.isZipped)(rv.headers) ? body : (0, zlib_1.gzipSync)(body) }; yield cache.set('payload:' + key, (0, payload_1.encodePayload)(payload), ttl); } } catch (e) { console.error('Error saving payload to cache', e); } finally { yield (0, cache_manager_1.unlock)(key, cache); } }); }; function CachedHandler(args, options) { return __awaiter(this, void 0, void 0, function* () { console.log('> Preparing cached handler'); // merge config const conf = (0, utils_1.mergeConfig)(options); // the cache if (!conf.cacheAdapter) { const { Adapter } = require('@next-boost/hybrid-disk-cache'); conf.cacheAdapter = new Adapter(); } const adapter = conf.cacheAdapter; const cache = yield adapter.init(); const renderer = (0, renderer_1.default)(); yield renderer.init(args); const plain = yield require(args.script).default(args.args); const metrics = new metrics_1.Metrics(); // init the child process for revalidate and cache purge return { handler: wrap(cache, conf, renderer, plain, metrics), cache, close: () => __awaiter(this, void 0, void 0, function* () { renderer.kill(); yield adapter.shutdown(); }), }; }); } exports.default = CachedHandler;