@@ -7,8 +7,28 @@ import type {
77import fp from 'fastify-plugin' ;
88import { ENV } from '../../env' ;
99import { encode } from '../../libs/encode' ;
10+ import { logger } from '../../libs/logger' ;
1011import { createRedisConnection } from '../../libs/utils/redis' ;
1112
13+ // Custom JSON serialization to handle BigInt values
14+ function serializeWithBigInt ( value : unknown ) : string {
15+ return JSON . stringify ( value , ( key , val ) => {
16+ if ( typeof val === 'bigint' ) {
17+ return { __type : 'bigint' , value : val . toString ( ) } ;
18+ }
19+ return val ;
20+ } ) ;
21+ }
22+
23+ function deserializeWithBigInt < T > ( json : string ) : T {
24+ return JSON . parse ( json , ( key , val ) => {
25+ if ( val && typeof val === 'object' && val . __type === 'bigint' ) {
26+ return BigInt ( val . value ) ;
27+ }
28+ return val ;
29+ } ) ;
30+ }
31+
1232const cacheRedisConnection = createRedisConnection (
1333 ENV . REDIS_URL ,
1434 ENV . REDIS_CLUSTER_MODE ,
@@ -106,7 +126,7 @@ const redisCachePlugin: FastifyPluginAsync<RedisCachePluginOptions> = async (
106126 req : FastifyRequest ,
107127 ) : RouteCacheOptions | null => {
108128 const rawCfg = req . routeOptions . config . cache ;
109- if ( ! rawCfg ) return null ;
129+ if ( ! rawCfg || ENV . NO_CACHE ) return null ;
110130
111131 if ( typeof rawCfg === 'boolean' ) {
112132 if ( ! rawCfg ) return null ;
@@ -142,15 +162,15 @@ const redisCachePlugin: FastifyPluginAsync<RedisCachePluginOptions> = async (
142162 ( cfg . key ?.( req ) ??
143163 encode . sha256 (
144164 `${ routeUrl } :${ req . raw . method } :` +
145- `${ JSON . stringify ( req . query ?? { } ) } :${ JSON . stringify ( req . body ?? { } ) } ` ,
165+ `${ JSON . stringify ( req . params ?? { } ) } : ${ JSON . stringify ( req . query ?? { } ) } :${ JSON . stringify ( req . body ?? { } ) } ` ,
146166 ) ) ;
147167
148168 req . __cacheKey = key ;
149169
150170 const cached = await redis . get ( key ) ;
151171 if ( ! cached ) return ;
152172
153- const entry : CacheEntry = JSON . parse ( cached ) ;
173+ const entry : CacheEntry = deserializeWithBigInt ( cached ) ;
154174 const ageSec = ( Date . now ( ) - entry . storedAt ) / 1000 ;
155175
156176 const isFresh = ageSec <= entry . ttlSeconds ;
@@ -228,7 +248,7 @@ const redisCachePlugin: FastifyPluginAsync<RedisCachePluginOptions> = async (
228248 const cfg : RouteCacheOptions =
229249 typeof rawCfg === 'boolean' ? { enabled : rawCfg } : rawCfg ;
230250
231- if ( cfg . enabled === false ) return payload ;
251+ if ( cfg . enabled === false || ENV . NO_CACHE ) return payload ;
232252
233253 if ( req . __cacheHit ) {
234254 return payload ;
@@ -260,7 +280,7 @@ const redisCachePlugin: FastifyPluginAsync<RedisCachePluginOptions> = async (
260280
261281 const expireSeconds = ttl + ( cfg . staleTtlSeconds ?? defaultStaleTtl ) ;
262282
263- await redis . setex ( key , expireSeconds , JSON . stringify ( entry ) ) ;
283+ await redis . setex ( key , expireSeconds , serializeWithBigInt ( entry ) ) ;
264284
265285 // For "real" client requests (not internal revalidation), set MISS header
266286 if ( req . headers [ 'x-cache-revalidate' ] !== '1' ) {
@@ -272,6 +292,53 @@ const redisCachePlugin: FastifyPluginAsync<RedisCachePluginOptions> = async (
272292 ) ;
273293} ;
274294
295+ export const maybeCache = async < T = unknown > (
296+ key : string ,
297+ fn : ( ) => Promise < T > ,
298+ opts : Pick < RouteCacheOptions , 'ttlSeconds' | 'enabled' > = { } ,
299+ ) : Promise < T > => {
300+ const enabled = opts . enabled ?? true ;
301+
302+ if ( ! enabled || ENV . NO_CACHE ) {
303+ return fn ( ) ;
304+ }
305+
306+ const redis = cacheRedisConnection ;
307+
308+ const ttl = opts . ttlSeconds ?? 30 ; // 30 seconds
309+
310+ const cacheKey = 'maybe-cache:fn:' + encode . sha256 ( key ) ;
311+
312+ const cached = await redis . get ( cacheKey ) ;
313+
314+ if ( cached ) {
315+ const entry : CacheEntry = deserializeWithBigInt ( cached ) ;
316+ const ageSec = ( Date . now ( ) - entry . storedAt ) / 1000 ;
317+
318+ const isFresh = ageSec <= entry . ttlSeconds ;
319+
320+ logger . info ( { key, ageSec, ttl : entry . ttlSeconds } , 'Cache hit' ) ;
321+
322+ if ( isFresh ) {
323+ return entry . payload as T ;
324+ }
325+ }
326+
327+ logger . info ( { key } , 'Cache miss, invoking function' ) ;
328+
329+ const entry = {
330+ payload : await fn ( ) ,
331+ headers : { } ,
332+ statusCode : 200 ,
333+ storedAt : Date . now ( ) ,
334+ ttlSeconds : ttl ,
335+ } ;
336+
337+ await redis . setex ( cacheKey , ttl , serializeWithBigInt ( entry ) ) ;
338+
339+ return entry . payload as T ;
340+ } ;
341+
275342export default fp ( redisCachePlugin , {
276343 name : 'cache-plugin' ,
277344} ) ;
0 commit comments