@@ -3,13 +3,54 @@ import { JobFilters } from "@/types/filters";
33import { Job } from "@/types/job" ;
44import serializeJob from "@/lib/utils" ;
55import logger from "@/lib/logger" ;
6+ import { LRUCache } from "lru-cache" ;
67
78const 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 */
195251export 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