Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions backend/src/common/config/cache.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { CacheModuleOptions, CacheStore } from '@nestjs/cache-manager';
import { redisStore } from 'cache-manager-redis-yet';
import { redisStore as redisIoStore } from 'cache-manager-ioredis';

/**
* Cache configuration factory for NestJS CacheModule.
*
* Uses Redis as the backing store (shares existing REDIS_URL).
* Falls back to in-memory cache when Redis is not configured (dev mode).
*
* TTL defaults:
* - Guilds leaderboard: 60s
* - Bounties list: 30s
* - General: 300s (5 min)
*/
export async function getCacheConfig(): Promise<CacheModuleOptions> {
const redisUrl = process.env.REDIS_URL;

if (!redisUrl || process.env.QUEUE_DISABLED === 'true') {
// Dev / no-Redis fallback: in-memory cache
return {
ttl: 5 * 1000, // 5 seconds default
max: 100, // max cached items
isGlobal: true,
};
}

// Production: Redis-backed cache
let store: CacheStore;

try {
store = await redisStore({
url: redisUrl,
ttl: 60 * 1000, // default 60s
isGlobal: true,
});
} catch {
// Fallback to ioredis if primary driver unavailable
store = await redisIoStore({
url: redisUrl,
ttl: 60 * 1000,
isGlobal: true,
});
}

return {
store,
isGlobal: true,
ttl: 60 * 1000,
};
}

/** Predefined TTL constants (in seconds) */
export const CACHE_TTL = {
GUILD_LEADERBOARD: 60, // 1 minute — high-demand, changes rarely
BOUNTIES_LIST: 30, // 30 seconds — moderate churn
GUILD_DETAILS: 120, // 2 minutes
USER_PROFILE: 300, // 5 minutes
DEFAULT: 300, // 5 minutes fallback
} as const;
16 changes: 16 additions & 0 deletions backend/src/common/decorators/cache.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { applyDecorators, SetMetadata, CacheTTL } from '@nestjs/common';

/** Custom metadata key for cache TTL override */
export const CACHE_TTL_META_KEY = 'cache:ttl';

/**
* Combined decorator: sets CacheKey + custom TTL metadata.
* Usage: @Cacheable('guilds:leaderboard', 60)
*/
export function Cacheable(key: string, ttlSeconds: number) {
return applyDecorators(
SetMetadata('CACHE_KEY', key),
SetMetadata(CACHE_TTL_META_KEY, ttlSeconds),
CacheTTL(ttlSeconds * 1000), // CacheTTL expects milliseconds
);
}
6 changes: 6 additions & 0 deletions backend/src/guild/guild.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
UseInterceptors,
HttpCode,
HttpStatus,
CacheKey,
CacheTTL,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
Expand Down Expand Up @@ -52,11 +54,15 @@ export class GuildController {
}

@Get('by-slug/:slug')
@CacheKey('guilds:slug')
@CacheTTL(120_000) // 2 minutes
async getBySlug(@Param('slug') slug: string): Promise<GuildDetailsDto> {
return this.guildService.getBySlug(slug);
}

@Get()
@CacheKey('guilds:search')
@CacheTTL(60_000) // 1 minute — high-demand endpoint
async search(@Query() query: SearchGuildDto) {
return this.guildService.searchGuilds(query.q, query.page, query.size);
}
Expand Down