@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
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());
});
};
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;