UNPKG

@next-boost/next-boost

Version:

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

100 lines (99 loc) 3.87 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()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.send = exports.serveCache = exports.unlock = exports.lock = exports.hasLock = void 0; const stream_1 = require("stream"); const payload_1 = require("./payload"); const utils_1 = require("./utils"); const MAX_WAIT = 10000; // 10 seconds const WAIT_INTERVAL = 10; // 10 ms function hasLock(key, cache) { return __awaiter(this, void 0, void 0, function* () { return (yield cache.has('lock:' + key)) === 'hit'; }); } exports.hasLock = hasLock; // mutex lock to prevent same page rendered more than once function lock(key, cache) { return __awaiter(this, void 0, void 0, function* () { yield cache.set('lock:' + key, Buffer.from('lock'), MAX_WAIT / 1000); // in seconds }); } exports.lock = lock; function unlock(key, cache) { return __awaiter(this, void 0, void 0, function* () { yield cache.del('lock:' + key); }); } exports.unlock = unlock; function serveCache(cache, key, forced) { return __awaiter(this, void 0, void 0, function* () { if (forced) return { status: 'force' }; try { const status = yield cache.has('payload:' + key); if (status === 'hit') { const payload = (0, payload_1.decodePayload)(yield cache.get('payload:' + key)); return { status: 'hit', payload }; } else if (status === 'miss') { const lock = yield hasLock(key, cache); // non first-time miss (the cache is being created), wait for the cache return !lock ? { status: 'miss' } : waitAndServe(key, cache); } else { // stale const payload = (0, payload_1.decodePayload)(yield cache.get('payload:' + key)); return { status: 'stale', payload }; } } catch (e) { console.error(`${key} cache error`, e); return { status: 'miss' }; } }); } exports.serveCache = serveCache; function waitAndServe(key, cache) { return __awaiter(this, void 0, void 0, function* () { while (yield hasLock(key, cache)) { // lock will expire yield (0, utils_1.sleep)(WAIT_INTERVAL); } const status = yield cache.has('payload:' + key); // still no cache after waiting for MAX_WAIT if (status === 'miss') { return { status: 'timeout' }; } else { const payload = (0, payload_1.decodePayload)(yield cache.get('payload:' + key)); return { status: 'fulfill', payload }; } }); } function send(payload, res) { const { body, headers } = payload; if (!body) { res.statusCode = 504; return res.end(); } for (const k in headers) { res.setHeader(k, headers[k]); } res.statusCode = 200; res.removeHeader('transfer-encoding'); res.setHeader('content-length', Buffer.byteLength(body)); res.setHeader('content-encoding', 'gzip'); const stream = new stream_1.PassThrough(); stream.pipe(res); stream.end(body); } exports.send = send;