Skip to content

Commit 4410ca3

Browse files
committed
Implement lru-cache for jobs
1 parent 7b9091a commit 4410ca3

File tree

3 files changed

+110
-19
lines changed

3 files changed

+110
-19
lines changed

frontend/package-lock.json

Lines changed: 20 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"dompurify": "^3.2.3",
2222
"isomorphic-dompurify": "^2.22.0",
2323
"jsdom": "^26.0.0",
24+
"lru-cache": "^11.2.2",
2425
"mongodb": "^6.14.2",
2526
"next": "15.1.7",
2627
"pino": "^9.11.0",

frontend/src/actions/jobs.fetch.ts

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,54 @@ import { JobFilters } from "@/types/filters";
33
import { Job } from "@/types/job";
44
import serializeJob from "@/lib/utils";
55
import logger from "@/lib/logger";
6+
import { LRUCache } from "lru-cache";
67

78
const PAGE_SIZE = 20;
89

9-
// Define the MongoJob interface with the correct DB field names.
10-
export interface MongoJob extends Omit<Job, "id"> {
11-
_id: ObjectId;
12-
is_sponsored: boolean;
10+
// Shared cache for job results
11+
type CacheValue = { jobs: Job[]; total: number } | Job;
12+
13+
const jobCache = new LRUCache<string, CacheValue>({
14+
max: 500,
15+
ttl: 1000 * 3600, // 1 hour
16+
allowStale: false,
17+
});
18+
19+
// Helper to normalize filters for cache key (handles string vs array for array fields)
20+
function normalizeFiltersForKey(
21+
filters: Partial<JobFilters>,
22+
): Record<string, string> {
23+
const arrayFields = [
24+
"workingRights[]",
25+
"locations[]",
26+
"industryFields[]",
27+
"jobTypes[]",
28+
];
29+
const normalized: Record<string, string> = {
30+
search: (filters.search || "").toLowerCase().trim(),
31+
page: (filters.page || 1).toString(),
32+
};
33+
34+
arrayFields.forEach((field) => {
35+
const val = filters[field as keyof Partial<JobFilters>];
36+
if (val !== undefined) {
37+
const arr = Array.isArray(val)
38+
? val
39+
: typeof val === "string"
40+
? [val]
41+
: [];
42+
normalized[field] = arr.sort().join(",");
43+
}
44+
});
45+
46+
// Include other scalar fields if present
47+
if (filters.jobTypes)
48+
normalized.jobTypes = (filters.jobTypes as string[]).sort().join(",");
49+
if (filters.locations)
50+
normalized.locations = (filters.locations as string[]).sort().join(",");
51+
// Add similar for others if needed, but since searchParams uses [] keys, prioritize those
52+
53+
return normalized;
1354
}
1455

1556
/**
@@ -103,15 +144,26 @@ export async function getJobs(
103144
minSponsors: number = -1,
104145
prioritySponsors: Array<string> = ["IMC", "Atlassian"],
105146
): Promise<{ jobs: Job[]; total: number }> {
147+
const page = filters.page || 1;
148+
const normalizedFilters = normalizeFiltersForKey(filters);
149+
const priorityStr = prioritySponsors.sort().join(",");
150+
const cacheKey = `jobs:${JSON.stringify(normalizedFilters)}:${page}:${minSponsors}:${priorityStr}`;
151+
152+
// Check cache first
153+
const cached = jobCache.get(cacheKey);
154+
if (cached) {
155+
logger.debug({ cacheKey }, "Returning cached jobs");
156+
return cached as { jobs: Job[]; total: number };
157+
}
158+
159+
logger.info(
160+
{ filters, minSponsors, prioritySponsors },
161+
"Fetching jobs with filters",
162+
);
163+
106164
return await withDbConnection(async (client) => {
107-
logger.info(
108-
{ filters, minSponsors, prioritySponsors },
109-
"Fetching jobs with filters",
110-
);
111165
const collection = client.db("default").collection("active_jobs");
112166
const query = buildJobQuery(filters);
113-
const page = filters.page || 1;
114-
const skip = (page - 1) * PAGE_SIZE;
115167
minSponsors = minSponsors === -1 ? (page == 1 ? 3 : 0) : minSponsors;
116168

117169
try {
@@ -120,18 +172,20 @@ export async function getJobs(
120172
collection
121173
.find(query)
122174
.sort({ created_at: -1 })
123-
.skip(skip)
175+
.skip((page - 1) * PAGE_SIZE)
124176
.limit(PAGE_SIZE)
125177
.toArray(),
126178
collection.countDocuments(query),
127179
]);
128180
logger.debug({ total }, "Fetched non-sponsored jobs");
129-
return {
181+
const result = {
130182
jobs: (jobs as MongoJob[])
131183
.map(serializeJob)
132184
.map((job) => ({ ...job, highlight: false })),
133185
total,
134186
};
187+
jobCache.set(cacheKey, result);
188+
return result;
135189
} else {
136190
const sponsoredQuery = { ...query, is_sponsored: true };
137191

@@ -158,7 +212,7 @@ export async function getJobs(
158212
collection
159213
.find(filteredQuery)
160214
.sort({ created_at: -1 })
161-
.skip(skip)
215+
.skip((page - 1) * PAGE_SIZE)
162216
.limit(PAGE_SIZE - sponsoredJobs.length)
163217
.toArray(),
164218
collection.countDocuments(query),
@@ -177,10 +231,12 @@ export async function getJobs(
177231
},
178232
"Fetched sponsored and other jobs",
179233
);
180-
return {
234+
const result = {
181235
jobs: (mergedJobs as MongoJob[]).map(serializeJob),
182236
total,
183237
};
238+
jobCache.set(cacheKey, result);
239+
return result;
184240
}
185241
} catch (error) {
186242
logger.error({ query, filters }, "Error fetching jobs");
@@ -193,7 +249,17 @@ export async function getJobs(
193249
* Fetches a single job by its id.
194250
*/
195251
export async function getJobById(id: string): Promise<Job | null> {
252+
const cacheKey = `job:${id}`;
253+
254+
// Check cache first
255+
const cached = jobCache.get(cacheKey);
256+
if (cached) {
257+
logger.debug({ id }, "Returning cached job");
258+
return cached as Job;
259+
}
260+
196261
logger.info({ id }, "Fetching job by ID");
262+
197263
return await withDbConnection(async (client) => {
198264
const collection = client.db("default").collection("active_jobs");
199265
const job = await collection.findOne({
@@ -205,6 +271,14 @@ export async function getJobById(id: string): Promise<Job | null> {
205271
return null;
206272
}
207273
logger.debug({ id }, "Job fetched successfully");
208-
return serializeJob(job as MongoJob);
274+
const serializedJob = serializeJob(job as MongoJob);
275+
jobCache.set(cacheKey, serializedJob);
276+
return serializedJob;
209277
});
210278
}
279+
280+
// Define the MongoJob interface with the correct DB field names.
281+
export interface MongoJob extends Omit<Job, "id"> {
282+
_id: ObjectId;
283+
is_sponsored: boolean;
284+
}

0 commit comments

Comments
 (0)