Skip to content
Merged
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
30 changes: 30 additions & 0 deletions src/constants/activity-feed-cache.constants.ts
Original file line number Diff line number Diff line change
@@ -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`;
96 changes: 96 additions & 0 deletions src/modules/activity/activity-cache-key.utils.ts
Original file line number Diff line number Diff line change
@@ -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}:*`];
}
5 changes: 4 additions & 1 deletion src/modules/activity/activity.routes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
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();

/**
* 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;
143 changes: 143 additions & 0 deletions src/modules/creators/creators-cache-key.utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading