@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
JavaScript
;
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;