Skip to content

Commit d7ab2a4

Browse files
authored
Merge pull request OFFER-HUB#698 from yhoungdev/feat/specialized_freelancer_search_hook
Feat/specialized freelancer search hook
2 parents 0e9a47b + 10de693 commit d7ab2a4

6 files changed

Lines changed: 2002 additions & 2 deletions

File tree

src/hooks/use-search-cache.ts

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
"use client"
2+
3+
import { useCallback, useRef, useMemo } from 'react'
4+
import {
5+
SearchCacheEntry,
6+
CacheOperations,
7+
SearchResults,
8+
SearchQuery ,
9+
SearchResultMetadata
10+
} from '../types/freelancer-search.types'
11+
import { SearchCache } from '../utils/search-performance'
12+
import { generateCacheKey } from '../utils/search-helpers'
13+
14+
interface UseSearchCacheOptions {
15+
maxSize?: number
16+
defaultTTL?: number
17+
enableCleanup?: boolean
18+
cleanupInterval?: number
19+
}
20+
21+
interface UseSearchCacheReturn extends CacheOperations {
22+
isEnabled: boolean
23+
enable: () => void
24+
disable: () => void
25+
getSize: () => number
26+
exportCache: () => Record<string, any>
27+
importCache: (data: Record<string, any>) => void
28+
}
29+
30+
31+
export function useSearchCache(options: UseSearchCacheOptions = {}): UseSearchCacheReturn {
32+
const {
33+
maxSize = 500,
34+
defaultTTL = 300000,
35+
enableCleanup = true,
36+
cleanupInterval = 60000
37+
} = options
38+
39+
const cacheRef = useRef<SearchCache>(new SearchCache(maxSize, defaultTTL))
40+
const cleanupIntervalRef = useRef<NodeJS.Timeout | null>(null)
41+
const isEnabledRef = useRef<boolean>(true)
42+
43+
const cache = cacheRef.current
44+
45+
const startCleanupInterval = useCallback(() => {
46+
if (cleanupIntervalRef.current) {
47+
clearInterval(cleanupIntervalRef.current)
48+
}
49+
50+
if (enableCleanup) {
51+
cleanupIntervalRef.current = setInterval(() => {
52+
cache.cleanup()
53+
}, cleanupInterval)
54+
}
55+
}, [cache, enableCleanup, cleanupInterval])
56+
57+
useMemo(() => {
58+
startCleanupInterval()
59+
return () => {
60+
if (cleanupIntervalRef.current) {
61+
clearInterval(cleanupIntervalRef.current)
62+
}
63+
}
64+
}, [startCleanupInterval])
65+
66+
67+
68+
69+
const get = useCallback(<T,>(key: string): SearchCacheEntry<T> | null => {
70+
if (!isEnabledRef.current) return null
71+
return cache.get<T>(key)
72+
}, [cache])
73+
74+
75+
const set = useCallback(<T,>(
76+
key: string | SearchQuery,
77+
data: T,
78+
ttl?: number,
79+
tags: string[] = []
80+
): void => {
81+
if (!isEnabledRef.current) return
82+
83+
const cacheKey = typeof key === 'string' ? key : generateCacheKey(key)
84+
cache.set(cacheKey, data, ttl, tags)
85+
}, [cache])
86+
87+
88+
const deleteEntry = useCallback((key: string | SearchQuery): boolean => {
89+
const cacheKey = typeof key === 'string' ? key : generateCacheKey(key)
90+
return cache.delete(cacheKey)
91+
}, [cache])
92+
93+
94+
const clear = useCallback((): void => {
95+
cache.clear()
96+
}, [cache])
97+
98+
99+
const invalidateByTag = useCallback((tag: string): number => {
100+
return cache.invalidateByTag(tag)
101+
}, [cache])
102+
103+
104+
const cleanup = useCallback((): number => {
105+
return cache.cleanup()
106+
}, [cache])
107+
108+
109+
const stats = useCallback(() => {
110+
return cache.stats()
111+
}, [cache])
112+
113+
114+
const enable = useCallback((): void => {
115+
isEnabledRef.current = true
116+
}, [])
117+
118+
119+
const disable = useCallback((): void => {
120+
isEnabledRef.current = false
121+
}, [])
122+
123+
124+
const getSize = useCallback((): number => {
125+
return cache.stats().size
126+
}, [cache])
127+
128+
129+
const exportCache = useCallback((): Record<string, any> => {
130+
const exported: Record<string, any> = {}
131+
132+
for (const [key, entry] of (cache as any).cache.entries()) {
133+
if (Date.now() < entry.timestamp + entry.ttl) {
134+
exported[key] = {
135+
data: entry.data,
136+
timestamp: entry.timestamp,
137+
ttl: entry.ttl,
138+
tags: entry.tags,
139+
version: entry.version
140+
}
141+
}
142+
}
143+
144+
return exported
145+
}, [cache])
146+
147+
148+
const importCache = useCallback((data: Record<string, any>): void => {
149+
for (const [key, entry] of Object.entries(data)) {
150+
if (entry && typeof entry === 'object' && entry.data && entry.timestamp) {
151+
const remainingTTL = (entry.timestamp + entry.ttl) - Date.now()
152+
if (remainingTTL > 0) {
153+
cache.set(key, entry.data, remainingTTL, entry.tags || [])
154+
}
155+
}
156+
}
157+
}, [cache])
158+
159+
const cacheOperations: CacheOperations = {
160+
get,
161+
set,
162+
delete: deleteEntry,
163+
clear,
164+
invalidateByTag,
165+
cleanup,
166+
stats
167+
}
168+
169+
return {
170+
...cacheOperations,
171+
isEnabled: isEnabledRef.current,
172+
enable,
173+
disable,
174+
getSize,
175+
exportCache,
176+
importCache
177+
}
178+
}
179+
180+
export function useSearchResultsCache(options: UseSearchCacheOptions = {}) {
181+
const cache = useSearchCache(options)
182+
183+
184+
const cacheResults = useCallback((
185+
query: SearchQuery,
186+
results: SearchResults,
187+
metadata?: Partial<SearchResultMetadata>
188+
): void => {
189+
const tags = [
190+
'search_results',
191+
`page_${query.page}`,
192+
`limit_${query.limit}`,
193+
...query.filters.skills.map(skill => `skill_${skill.name}`),
194+
query.filters.location?.city ? `city_${query.filters.location.city}` : '',
195+
query.filters.priceRange?.currency ? `currency_${query.filters.priceRange.currency}` : ''
196+
].filter(Boolean)
197+
198+
cache.set(
199+
generateCacheKey(query),
200+
{
201+
...results,
202+
metadata: metadata ? { ...results.metadata, ...metadata } : results.metadata
203+
},
204+
undefined,
205+
tags
206+
)
207+
}, [cache])
208+
209+
210+
const getCachedResults = useCallback((query: SearchQuery): SearchResults | null => {
211+
const cached = cache.get<SearchResults>(generateCacheKey(query))
212+
return cached ? cached.data : null
213+
}, [cache])
214+
215+
const invalidateByFilter = useCallback((filterType: string, filterValue?: string): void => {
216+
const tag = filterValue ? `${filterType}_${filterValue}` : filterType
217+
cache.invalidateByTag(tag)
218+
}, [cache])
219+
220+
221+
const prewarmCache = useCallback(async (
222+
popularQueries: SearchQuery[],
223+
searchFunction: (query: SearchQuery) => Promise<SearchResults>
224+
): Promise<void> => {
225+
const prewarmPromises = popularQueries.map(async (query) => {
226+
const existing = getCachedResults(query)
227+
if (!existing) {
228+
try {
229+
const results = await searchFunction(query)
230+
//@ts-ignore
231+
cacheResults(query, results, { prewarmed: true })
232+
} catch (error) {
233+
console.warn('Failed to prewarm cache for query:', query, error)
234+
}
235+
}
236+
})
237+
238+
await Promise.allSettled(prewarmPromises)
239+
}, [getCachedResults, cacheResults])
240+
241+
242+
const getCacheMetrics = useCallback(() => {
243+
const stats = cache.stats()
244+
return {
245+
...stats,
246+
effectiveness: stats.hitRate > 0.3 ? 'high' : stats.hitRate > 0.1 ? 'medium' : 'low',
247+
recommendation: stats.hitRate < 0.1
248+
? 'Consider increasing TTL or adjusting cache strategy'
249+
: stats.hitRate > 0.7
250+
? 'Cache is performing well'
251+
: 'Cache performance is acceptable'
252+
}
253+
}, [cache])
254+
255+
return {
256+
...cache,
257+
cacheResults,
258+
getCachedResults,
259+
invalidateByFilter,
260+
prewarmCache,
261+
getCacheMetrics
262+
}
263+
}

0 commit comments

Comments
 (0)