Skip to content

Commit ddbe1f5

Browse files
committed
feat(update): improved cache handler
1 parent c5f086b commit ddbe1f5

2 files changed

Lines changed: 62 additions & 69 deletions

File tree

src/fetchers/github.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface FetcherProps {
1111
const getGithubApiUrl = () => process.env.GITHUB_API_URL;
1212
const getGithubAccessToken = () => process.env.GITHUB_PAT;
1313

14-
const cache = new CacheHandler({ ttl: 5 * 60 * 1000 });
14+
const cache = new CacheHandler({ ttl: 5 * 60 * 1000, maxSize: 500 });
1515

1616
export const githubFetcher = async ({ repoOwner, repoName, jsonPath, slug, type }: FetcherProps) => {
1717
const apiUrl = getGithubApiUrl();

src/lib/cache.js

Lines changed: 61 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
const cache = new Map();
21
const logger = require('./logger').default;
32

43
module.exports = class CacheHandler {
@@ -9,109 +8,95 @@ module.exports = class CacheHandler {
98
* @param {number} [options.maxSize=100] - Maximum number of cache entries.
109
*/
1110
constructor(options = {}) {
11+
this.cache = new Map(); // Stores cache entries
12+
this.cacheKeys = []; // Tracks cache keys for LRU eviction
1213
this.options = {
1314
ttl: options.ttl || 60 * 1000, // Default: 1 minute
14-
maxSize: options.maxSize || 100, // Limit max items
15+
maxSize: options.maxSize || 100, // Default max cache size
1516
...options,
1617
};
18+
19+
// Periodically clean expired cache entries
20+
setInterval(() => this.cleanupExpiredEntries(), this.options.ttl);
1721
}
1822

1923
/**
2024
* Retrieves a value from the cache.
2125
* @param {string} key - The cache key.
2226
* @returns {Promise<any>} The cached value or null if not found or expired.
27+
* @throws {TypeError} If the key is not a string.
2328
*/
2429
async get(key) {
25-
const entry = cache.get(key);
30+
if (typeof key !== 'string') {
31+
throw new TypeError('Cache key must be a string');
32+
}
2633

34+
const entry = this.cache.get(key);
2735
if (!entry) {
28-
logger.cache({
29-
message: `Unable to find cache for key: ${key}`,
30-
service: 'CACHE',
31-
method: 'get',
32-
cacheState: 'MISS',
33-
level: 'warn'
34-
});
36+
logger.cache({ message: `Cache miss: ${key}`, service: 'CACHE', method: 'get', cacheState: 'MISS', level: 'warn' });
3537
return null;
3638
}
3739

3840
// Check expiration
3941
if (Date.now() - entry.lastModified > this.options.ttl) {
40-
cache.delete(key);
41-
logger.cache({
42-
message: `The cache for key: ${key} has expired`,
43-
service: 'CACHE',
44-
method: 'get',
45-
cacheState: 'EXPIRED',
46-
level: 'error'
47-
});
42+
this.cache.delete(key);
43+
this.cacheKeys.splice(this.cacheKeys.indexOf(key), 1);
44+
logger.cache({ message: `Cache expired: ${key}`, service: 'CACHE', method: 'get', cacheState: 'EXPIRED', level: 'error' });
4845
return null;
4946
}
5047

51-
logger.cache({
52-
message: `Located cache for key: ${key}`,
53-
service: 'CACHE',
54-
method: 'get',
55-
cacheState: 'HIT',
56-
level: 'info'
57-
});
48+
// Move key to end (most recently used)
49+
this.cacheKeys.splice(this.cacheKeys.indexOf(key), 1);
50+
this.cacheKeys.push(key);
5851

52+
logger.cache({ message: `Cache hit: ${key}`, service: 'CACHE', method: 'get', cacheState: 'HIT', level: 'info' });
5953
return entry.value;
6054
}
6155

6256
/**
63-
* Sets a value in the cache.
57+
* Stores a value in the cache.
58+
* If the cache exceeds its max size, it removes the least recently used (LRU) entry.
6459
* @param {string} key - The cache key.
6560
* @param {any} data - The data to cache.
6661
* @param {Object} [ctx={}] - The context object.
6762
* @param {string[]} [ctx.tags=[]] - Tags associated with the cache entry.
63+
* @throws {TypeError} If the key is not a string.
64+
* @throws {Error} If the data is undefined.
6865
*/
6966
async set(key, data, ctx = {}) {
70-
if (cache.size >= this.options.maxSize) {
71-
// Evict oldest entry
72-
const oldestKey = [...cache.keys()][0];
73-
cache.delete(oldestKey);
74-
logger.cache({
75-
message: `Evicted cache for key: ${oldestKey}`,
76-
service: 'CACHE',
77-
method: 'set',
78-
cacheState: 'EVICTED',
79-
level: 'warn'
80-
});
67+
if (typeof key !== 'string') {
68+
throw new TypeError('Cache key must be a string');
69+
}
70+
if (data === undefined) {
71+
throw new Error('Cannot cache undefined value');
8172
}
8273

83-
cache.set(key, {
84-
value: data,
85-
lastModified: Date.now(),
86-
tags: ctx.tags || [],
87-
});
74+
// If cache reaches max size, remove least recently used (LRU) entry
75+
if (this.cache.size >= this.options.maxSize) {
76+
const oldestKey = this.cacheKeys.shift(); // Remove LRU entry
77+
this.cache.delete(oldestKey);
78+
logger.cache({ message: `Evicted LRU cache: ${oldestKey}`, service: 'CACHE', method: 'set', cacheState: 'EVICTED', level: 'warn' });
79+
}
80+
81+
// Store new entry
82+
this.cache.set(key, { value: data, lastModified: Date.now(), tags: ctx.tags || [] });
83+
this.cacheKeys.push(key); // Track for LRU
8884

89-
logger.cache({
90-
message: `Set cache for key: ${key}`,
91-
service: 'CACHE',
92-
method: 'set',
93-
cacheState: 'SET',
94-
level: 'info'
95-
});
85+
logger.cache({ message: `Cache set: ${key}`, service: 'CACHE', method: 'set', cacheState: 'SET', level: 'info' });
9686
}
9787

9888
/**
99-
* Revalidates cache entries by tags.
100-
* @param {string|string[]} tags - The tags to revalidate.
89+
* Invalidates cache entries associated with a specific tag or multiple tags.
90+
* @param {string|string[]} tags - The tag(s) to invalidate.
10191
*/
10292
async revalidateTag(tags) {
10393
tags = Array.isArray(tags) ? tags : [tags];
10494

105-
for (const [key, value] of cache) {
95+
for (const [key, value] of this.cache) {
10696
if (value.tags.some(tag => tags.includes(tag))) {
107-
cache.delete(key);
108-
logger.cache({
109-
message: `Invalidated cache for key: ${key}`,
110-
service: 'CACHE',
111-
method: 'revalidateTag',
112-
cacheState: 'INVALIDATE',
113-
level: 'info'
114-
});
97+
this.cache.delete(key);
98+
this.cacheKeys.splice(this.cacheKeys.indexOf(key), 1);
99+
logger.cache({ message: `Cache invalidated: ${key}`, service: 'CACHE', method: 'revalidateTag', cacheState: 'INVALIDATE', level: 'info' });
115100
}
116101
}
117102
}
@@ -120,13 +105,21 @@ module.exports = class CacheHandler {
120105
* Clears all cache entries.
121106
*/
122107
async clear() {
123-
cache.clear();
124-
logger.cache({
125-
message: `Cleared all cache entries`,
126-
service: 'CACHE',
127-
method: 'clear',
128-
cacheState: 'CLEAR',
129-
level: 'info'
130-
});
108+
this.cache.clear();
109+
this.cacheKeys = [];
110+
logger.cache({ message: `All cache cleared`, service: 'CACHE', method: 'clear', cacheState: 'CLEAR', level: 'info' });
111+
}
112+
113+
/**
114+
* Periodically removes expired cache entries.
115+
*/
116+
cleanupExpiredEntries() {
117+
for (const [key, entry] of this.cache) {
118+
if (Date.now() - entry.lastModified > this.options.ttl) {
119+
this.cache.delete(key);
120+
this.cacheKeys.splice(this.cacheKeys.indexOf(key), 1);
121+
logger.cache({ message: `Auto-cleaned expired cache: ${key}`, service: 'CACHE', method: 'cleanup', cacheState: 'EXPIRED', level: 'info' });
122+
}
123+
}
131124
}
132125
};

0 commit comments

Comments
 (0)