diff --git a/src/constants/activity-feed-cache.constants.ts b/src/constants/activity-feed-cache.constants.ts new file mode 100644 index 0000000..ab1dba8 --- /dev/null +++ b/src/constants/activity-feed-cache.constants.ts @@ -0,0 +1,30 @@ +/** + * Cache settings for the activity feed endpoint. + * + * These constants define cache behavior for the public activity feed + * endpoint, which lists activities with optional filtering and pagination. + */ +import { PUBLIC_ENDPOINT_CACHE_SECONDS } from './public-endpoint-cache.constants'; + +/** + * Max-age (seconds) for activity feed responses. + * + * Activity feeds are frequently updated but can be cached for short periods + * to reduce database load. A 2-minute cache provides a balance between + * freshness and performance. + */ +export const ACTIVITY_FEED_CACHE_MAX_AGE_SECONDS = PUBLIC_ENDPOINT_CACHE_SECONDS.short; + +/** + * Cache control preset for the activity feed endpoint. + */ +export const ACTIVITY_FEED_CACHE_PRESET = { + maxAge: ACTIVITY_FEED_CACHE_MAX_AGE_SECONDS, + type: 'public' as const, + staleIfError: 86400, // 24 hours +} as const; + +/** + * Full `Cache-Control` header value for the activity feed endpoint. + */ +export const ACTIVITY_FEED_CACHE_CONTROL_HEADER = `public, max-age=${ACTIVITY_FEED_CACHE_MAX_AGE_SECONDS}, stale-if-error=86400`; diff --git a/src/modules/activity/activity-cache-key.utils.ts b/src/modules/activity/activity-cache-key.utils.ts new file mode 100644 index 0000000..54300c7 --- /dev/null +++ b/src/modules/activity/activity-cache-key.utils.ts @@ -0,0 +1,96 @@ +/** + * Cache key builder for creator activity feed responses. + * + * This utility generates deterministic cache keys that include all + * filter and pagination inputs to ensure cache invalidation works correctly. + */ +import { ActivityQueryType } from './activity.schemas'; + +/** + * Builds a cache key for the activity feed endpoint. + * + * The key includes all query parameters to ensure that different + * filter/pagination combinations have separate cache entries. + * + * @param query - The parsed activity feed query parameters + * @returns A deterministic cache key string + * + * @example + * ```ts + * const key = buildActivityFeedCacheKey({ + * limit: 10, + * offset: 0, + * creatorId: 'abc123', + * actor: 'xyz789', + * type: 'KEY_BOUGHT' + * }); + * // Returns: "activity:limit:10:offset:0:creatorId:abc123:actor:xyz789:type:KEY_BOUGHT" + * ``` + */ +export function buildActivityFeedCacheKey(query: ActivityQueryType): string { + const parts: string[] = ['activity']; + + // Add pagination parameters + parts.push(`limit:${query.limit}`); + parts.push(`offset:${query.offset}`); + + // Add filter parameters if present + if (query.creatorId) { + parts.push(`creatorId:${query.creatorId}`); + } + + if (query.actor) { + parts.push(`actor:${query.actor}`); + } + + if (query.type) { + parts.push(`type:${query.type}`); + } + + return parts.join(':'); +} + +/** + * Cache invalidation touchpoints for the activity feed. + * + * The activity feed cache should be invalidated when: + * - A creator is registered (CREATOR_REGISTERED event) + * - A key is bought (KEY_BOUGHT event) + * - A key is sold (KEY_SOLD event) + * - A profile is updated (PROFILE_UPDATED event) + * + * These events are the same as the activity feed types, so any new activity + * that would appear in the feed should invalidate the cache. + * + * Implementation note: Cache invalidation should be triggered in the + * respective event handlers or service methods that create these activities. + * Use the cache key builder to determine which keys to invalidate based on + * the affected creator or actor. + */ +export const ACTIVITY_FEED_CACHE_INVALIDATION_TOUCHPOINTS = { + CREATOR_REGISTERED: 'creator:registered', + KEY_BOUGHT: 'key:bought', + KEY_SOLD: 'key:sold', + PROFILE_UPDATED: 'profile:updated', +} as const; + +/** + * Builds cache keys for invalidation based on a creator ID. + * + * When an activity event occurs for a specific creator, this helper + * generates all cache key patterns that should be invalidated for that creator. + * + * @param creatorId - The creator ID whose cache should be invalidated + * @returns Array of cache key patterns to invalidate + * + * @example + * ```ts + * const keys = buildActivityFeedInvalidationKeys('abc123'); + * // Returns: ['activity:*:creatorId:abc123:*'] + * ``` + */ +export function buildActivityFeedInvalidationKeys(creatorId: string): string[] { + // Invalidate all activity feed entries for this creator + // regardless of pagination or other filters + return [`activity:*:creatorId:${creatorId}:*`]; +} diff --git a/src/modules/activity/activity.routes.ts b/src/modules/activity/activity.routes.ts index d2c6c61..8c3be47 100644 --- a/src/modules/activity/activity.routes.ts +++ b/src/modules/activity/activity.routes.ts @@ -1,5 +1,7 @@ import { Router } from 'express'; import { httpGetActivityFeed } from './activity.controllers'; +import { cacheControl } from '../../middlewares/cache-control.middleware'; +import { ACTIVITY_FEED_CACHE_PRESET } from '../../constants/activity-feed-cache.constants'; const activityRouter = Router(); @@ -7,7 +9,8 @@ const activityRouter = Router(); * GET /api/v1/activity * * Public activity feed with optional filtering by creator, actor, or type. + * Cached for 2 minutes to reduce database load while maintaining reasonable freshness. */ -activityRouter.get('/', httpGetActivityFeed); +activityRouter.get('/', cacheControl(ACTIVITY_FEED_CACHE_PRESET), httpGetActivityFeed); export default activityRouter; diff --git a/src/modules/creators/creators-cache-key.utils.ts b/src/modules/creators/creators-cache-key.utils.ts new file mode 100644 index 0000000..6a20c96 --- /dev/null +++ b/src/modules/creators/creators-cache-key.utils.ts @@ -0,0 +1,143 @@ +/** + * Cache key builder for creator feed responses. + * + * This utility generates deterministic cache keys that include all + * filter and pagination inputs to ensure cache invalidation works correctly. + */ +import { CreatorListQueryType } from './creators.schemas'; + +/** + * Builds a cache key for the creator feed endpoint. + * + * The key includes all query parameters to ensure that different + * filter/pagination combinations have separate cache entries. + * + * @param query - The parsed creator feed query parameters + * @returns A deterministic cache key string + * + * @example + * ```ts + * const key = buildCreatorFeedCacheKey({ + * limit: 20, + * offset: 0, + * sort: 'createdAt', + * order: 'desc', + * verified: true, + * search: 'example', + * include: ['stats'] + * }); + * // Returns: "creators:limit:20:offset:0:sort:createdAt:order:desc:verified:true:search:example:include:stats" + * ``` + */ +export function buildCreatorFeedCacheKey(query: CreatorListQueryType): string { + const parts: string[] = ['creators']; + + // Add pagination parameters + parts.push(`limit:${query.limit}`); + parts.push(`offset:${query.offset}`); + + // Add sorting parameters + parts.push(`sort:${query.sort}`); + parts.push(`order:${query.order}`); + + // Add filter parameters if present + if (query.verified !== undefined) { + parts.push(`verified:${query.verified}`); + } + + if (query.search !== undefined && query.search !== '') { + parts.push(`search:${query.search}`); + } + + if (query.include !== undefined && query.include.length > 0) { + parts.push(`include:${query.include.join(',')}`); + } + + return parts.join(':'); +} + +/** + * Cache invalidation touchpoints for the creator feed. + * + * The creator feed cache should be invalidated when: + * - A creator is registered (new creator added to the feed) + * - A creator profile is updated (display name, bio, avatar, etc.) + * - A creator's verification status changes + * - A creator's keys supply or floor price changes + * - A creator's stats change + * + * These events affect the creator feed display and should trigger + * cache invalidation for the affected creator or the entire feed. + * + * Implementation note: Cache invalidation should be triggered in the + * respective service methods that create/update creators. Use the cache + * key builder to determine which keys to invalidate based on the affected + * creator or filter combinations. + */ +export const CREATOR_FEED_CACHE_INVALIDATION_TOUCHPOINTS = { + CREATOR_REGISTERED: 'creator:registered', + CREATOR_PROFILE_UPDATED: 'creator:profile:updated', + CREATOR_VERIFICATION_CHANGED: 'creator:verification:changed', + CREATOR_KEYS_UPDATED: 'creator:keys:updated', + CREATOR_STATS_UPDATED: 'creator:stats:updated', +} as const; + +/** + * Builds cache keys for invalidation based on a creator ID. + * + * When a creator event occurs (profile update, verification change, etc.), + * this helper generates all cache key patterns that should be invalidated + * for that creator. + * + * @param creatorId - The creator ID whose cache should be invalidated + * @returns Array of cache key patterns to invalidate + * + * @example + * ```ts + * const keys = buildCreatorFeedInvalidationKeys('abc123'); + * // Returns: ['creators:*:*:*:*:*:*:*'] (all creator feed entries) + * ``` + */ +export function buildCreatorFeedInvalidationKeys(_creatorId?: string): string[] { + // Since the creator feed includes all creators and supports various filters, + // we invalidate all creator feed entries when any creator changes. + // This is a conservative approach that ensures cache consistency. + return ['creators:*']; +} + +/** + * Builds cache keys for invalidation based on specific filter combinations. + * + * When a specific filter-relevant change occurs (e.g., verification status change), + * this helper generates cache key patterns for affected filter combinations. + * + * @param filters - Object containing filter values that changed + * @returns Array of cache key patterns to invalidate + * + * @example + * ```ts + * const keys = buildCreatorFeedFilterInvalidationKeys({ verified: true }); + * // Returns: ['creators:*:*:*:*:verified:true:*'] + * ``` + */ +export function buildCreatorFeedFilterInvalidationKeys(filters: { + verified?: boolean; + search?: string; +}): string[] { + const patterns: string[] = []; + + if (filters.verified !== undefined) { + patterns.push(`creators:*:*:*:*:verified:${filters.verified}:*`); + } + + if (filters.search !== undefined) { + patterns.push(`creators:*:*:*:*:search:${filters.search}:*`); + } + + // If no specific filters, invalidate all + if (patterns.length === 0) { + return ['creators:*']; + } + + return patterns; +}