Skip to content

Commit f9e9f6e

Browse files
perf(cache): stable cache keys and graceful shutdown fix (#95)
- Use JSON.stringify with replacer for stable key ordering This preserves JSON.stringify semantics (Date.toJSON, undefined omission, etc.) while ensuring {a:1, b:2} and {b:2, a:1} produce the same cache key - Call unref() on cleanup interval so it doesn't prevent graceful shutdown Without this, the hourly interval keeps the Node.js event loop alive even when the server is trying to shut down 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <[email protected]>
1 parent 025e691 commit f9e9f6e

File tree

1 file changed

+30
-1
lines changed

1 file changed

+30
-1
lines changed

backend/services/cacheService.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,39 @@ const CACHE_TTL = {
2626
CLIMATE_STATS: msToMinutes(TIMEOUTS.CACHE.CLIMATE_TTL, 43200),
2727
};
2828

29+
/**
30+
* Stable JSON stringify with sorted keys
31+
* Uses JSON.stringify with a replacer to ensure consistent key ordering.
32+
* Handles Date (via toJSON), undefined (omitted), and other JSON.stringify semantics.
33+
* @param {any} obj - Object to stringify
34+
* @returns {string} Stable JSON string
35+
*/
36+
function stableStringify(obj) {
37+
// Use JSON.stringify with a replacer that sorts object keys
38+
// This preserves JSON.stringify semantics (Date.toJSON, undefined handling, etc.)
39+
return JSON.stringify(obj, (key, value) => {
40+
// For objects (not arrays, not null), return a new object with sorted keys
41+
if (value && typeof value === 'object' && !Array.isArray(value)) {
42+
return Object.keys(value)
43+
.sort()
44+
.reduce((sorted, k) => {
45+
sorted[k] = value[k];
46+
return sorted;
47+
}, {});
48+
}
49+
return value;
50+
});
51+
}
52+
2953
/**
3054
* Generate cache key from request parameters
55+
* Uses stable stringify to ensure consistent keys regardless of object key order
3156
* @param {string} apiSource - API source identifier (e.g., 'visualcrossing')
3257
* @param {object} params - Request parameters
3358
* @returns {string} MD5 hash cache key
3459
*/
3560
function generateCacheKey(apiSource, params) {
36-
const paramsString = JSON.stringify(params);
61+
const paramsString = stableStringify(params);
3762
return crypto.createHash('md5').update(`${apiSource}:${paramsString}`).digest('hex');
3863
}
3964

@@ -212,6 +237,10 @@ if (process.env.NODE_ENV !== 'test') {
212237
},
213238
60 * 60 * 1000
214239
); // 1 hour
240+
241+
// Allow process to exit even if this interval is still running
242+
// Without this, the interval keeps the event loop alive during graceful shutdown
243+
cacheCleanupInterval.unref();
215244
}
216245

217246
module.exports = {

0 commit comments

Comments
 (0)