Skip to content

Commit 1a7045f

Browse files
committed
add stale when revalidate
1 parent f752644 commit 1a7045f

2 files changed

Lines changed: 103 additions & 12 deletions

File tree

.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ USE_CACHE="true"
88
CACHE_TTL="60"
99
DEBUG="true"
1010
PORT="3001"
11-
MINIFY="true"
11+
MINIFY="false"
12+
USE_STALE="true"

FPC.js

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,19 @@ const IGNORED_URLS = ["/customer", "/media", "/admin", "/checkout"];
2727
const HTTPS = getEnvBoolean("HTTPS", true);
2828
const HOST = process.env.HOST || false;
2929
const MINIFY = getEnvBoolean("MINIFY");
30+
const USE_STALE = getEnvBoolean("USE_STALE", true);
3031

3132
// APCu Equivalent (Node.js in-memory cache)
3233
const cache = new NodeCache({ stdTTL: CACHE_TTL }); // 5 min cache
3334

35+
if (USE_STALE) {
36+
cache.on("del"/*expired*/, (key, value) => {
37+
console.log("EXPIRED:" + key);
38+
value.expired = true;
39+
cache.set(key, value, CACHE_TTL * 10);
40+
});
41+
}
42+
3443
// Start HTTP Server
3544
const server = http.createServer(async (req, res) => {
3645
const startTime = process.hrtime();
@@ -51,7 +60,23 @@ const server = http.createServer(async (req, res) => {
5160
}
5261

5362
// Try APCu like (NodeCache) First
63+
var cacheInfo = null;
5464
let cachedPage = USE_CACHE ? cache.get(cacheKey) : null;
65+
if (cachedPage) {
66+
cacheInfo = getCacheInfo(cacheKey);
67+
res.setHeader("Node-Cache", "true");
68+
console.log('NodeCACHE: HIT');
69+
} else if (USE_STALE) {
70+
// when cache expired we need check stale twice
71+
cachedPage = cache.get(cacheKey)
72+
if (cachedPage) {
73+
cacheInfo = getCacheInfo(cacheKey);
74+
res.setHeader("Node-Stale", "true");
75+
console.log('NodeCACHE: STALE');
76+
} else {
77+
console.log('NodeCACHE: MISS');
78+
}
79+
}
5580

5681
if (!cachedPage) {
5782
const redisStartTime = process.hrtime();
@@ -60,14 +85,11 @@ const server = http.createServer(async (req, res) => {
6085
const redisTimeMs = (redisEndTime[1] / 1e6).toFixed(2);
6186
if (DEBUG) res.setHeader("Server-Timing", `redis;dur=${redisTimeMs}`);
6287

63-
6488
if (!cachedPage) {
6589
if (DEBUG) res.setHeader("Fast-Cache", "MISS");
6690
return sendNotFound(res);
6791
}
6892

69-
cachedPage = await uncompress(cachedPage);
70-
cachedPage = JSON.parse(cachedPage);
7193
if (USE_CACHE) cache.set(cacheKey, cachedPage, CACHE_TTL);
7294
} else {
7395
if (DEBUG) res.setHeader("Fast-Cache", "HIT (NodeCACHE)");
@@ -82,11 +104,13 @@ const server = http.createServer(async (req, res) => {
82104

83105
let content = cachedPage.content;
84106
if (MINIFY && !cachedPage.minified) {
107+
(async () => {
85108
content = await minifyHTML(content);
86-
cachedPage.content = content;
87-
cachedPage.minified = true;
88-
if (USE_CACHE) cache.set(cacheKey, cachedPage, CACHE_TTL);
89-
//ToDo: resave minified to Redis ;)
109+
cachedPage.content = content;
110+
cachedPage.minified = true;
111+
if (USE_CACHE) cache.set(cacheKey, cachedPage, CACHE_TTL);
112+
//ToDo: resave minified to Redis ;)
113+
})();
90114
}
91115

92116
// Measure Total Execution Time
@@ -97,10 +121,30 @@ const server = http.createServer(async (req, res) => {
97121
console.log("FPC-TIME:[" + req.url + "]->" + (endTime[1] / 1e6).toFixed(2) + "ms");
98122

99123
res.writeHead(200, { "Content-Type": "text/html" });
124+
125+
if (USE_STALE && cacheInfo !== null && cacheInfo.stale) {
126+
(async () => {
127+
try {
128+
console.log('Fetched new data');
129+
const newContent = await fetchOriginalData(req, {'Refresh': '1'});
130+
if (newContent) {
131+
let oldCache = cache.get(cacheKey);
132+
let newCache = await getRedisValue(cacheKey, "d");
133+
if (oldCache && newCache) {
134+
// Update the cache with new data
135+
console.log('SET new data');
136+
cache.set(cacheKey, newCache, CACHE_TTL);
137+
}
138+
}
139+
} catch (error) {
140+
console.error('Error fetching new data:', error);
141+
}
142+
})();
143+
}
144+
console.log('----Response Return---');
100145
res.end(content);
101146
} catch (err) {
102147
if (DEBUG) {
103-
res.setHeader("FPC-ERROR", "Exception");
104148
console.error("FPC Error:", err);
105149
}
106150
sendNotFound(res);
@@ -110,6 +154,8 @@ const server = http.createServer(async (req, res) => {
110154
// Function to Check if Request is Cacheable
111155
function isCached(req) {
112156
if (req.method !== "GET") return false;
157+
// Bypass cache for refresh requests
158+
if (req.headers["refresh"] === "1") return false;
113159
return !IGNORED_URLS.some(url => req.url.startsWith(url));
114160
}
115161

@@ -163,10 +209,16 @@ function sendNotFound(res) {
163209
res.end(JSON.stringify({ error: "Not Cached" }));
164210
}
165211

166-
async function getRedisValue(key, field) {
212+
async function getRedisValue(key, field = "d") {
167213
try {
168-
const value = await redis.hget(key, field); // Use HGET instead of HGETALL
169-
// console.log("HGET Response:", JSON.parse(value));
214+
let value = await redis.hget(key, field); // Use HGET instead of HGETALL
215+
console.log("HGET:", Boolean(value));
216+
if (!value) {
217+
return false;
218+
}
219+
220+
value = await uncompress(value);
221+
value = JSON.parse(value);
170222
return value;
171223
} catch (err) {
172224
console.error("Redis Error:", err);
@@ -212,6 +264,44 @@ async function minifyHTML(htmlContent) {
212264
});
213265
}
214266

267+
// Function to get the TTL and saved time of a cached object
268+
function getCacheInfo(key) {
269+
const now = Date.now();
270+
const ttl = cache.getTtl(key); // Get the TTL of the key timestamp when expired
271+
let stale = false;
272+
if (cache.get(key).expired) {
273+
stale = true;
274+
}
275+
276+
if (ttl !== undefined) {
277+
console.log(`Key: ${key}`);
278+
console.log(`TTL: ${ttl} ms`);
279+
console.log(`Expired:`, stale);
280+
} else {
281+
//console.log(`Key: ${key} not found in cache.`);
282+
}
283+
return {stale, ttl}
284+
}
285+
286+
// Function to fetch original data with additional headers
287+
async function fetchOriginalData(req, additionalHeaders = {}) {
288+
try {
289+
const url = getUrl(req);
290+
const originalHeaders = req.headers;
291+
// Merge original headers with additional headers
292+
const headers = { ...originalHeaders, ...additionalHeaders };
293+
294+
const response = await fetch(url, { headers });
295+
if (!response.ok) {
296+
return false;
297+
}
298+
return response;
299+
} catch (error) {
300+
console.error('Error fetching data:', error);
301+
return null;
302+
}
303+
}
304+
215305
// Start Server
216306
const PORT = process.env.PORT || 3001;
217307
server.listen(PORT, () => console.log(`🚀 Node.js FPC Server running on port ${PORT}`));

0 commit comments

Comments
 (0)