| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- /**
- * Redis cache key semantic types and constants.
- *
- * This file provides TypeScript types and constants to enforce semantic correctness
- * when working with Redis cache keys. Use these to ensure:
- * - Video ID keys always contain video IDs (strings), never JSON
- * - Tag keys always contain Tag JSON, not video IDs
- * - Operations match the key type (LRANGE for LIST, ZREVRANGE for ZSET, etc.)
- *
- * SEE: libs/common/src/cache/CACHE_SEMANTICS.md for detailed documentation
- */
- /**
- * Marker type for video ID strings (Mongo ObjectId format).
- * Use this to distinguish video IDs from other string types.
- *
- * Example:
- * const videoIds: VideoId[] = ['64a2b3c4d5e6f7g8h9i0j1k2', 'abc123...']
- */
- export type VideoId = string & { readonly __videoId: true };
- /**
- * Marker type for tag ID strings.
- */
- export type TagId = string & { readonly __tagId: true };
- /**
- * Marker type for category ID strings.
- */
- export type CategoryId = string & { readonly __categoryId: true };
- /**
- * Marker type for channel ID strings.
- */
- export type ChannelId = string & { readonly __channelId: true };
- /**
- * Helper to create a VideoId (for cases where you have a validated string).
- *
- * Example:
- * const videoId = asVideoId('64a2b3c4d5e6f7g8h9i0j1k2');
- * const listKey = tsCacheKeys.video.categoryList('ch-1');
- * const videoIds = await redis.lrange(listKey, 0, -1) as VideoId[];
- */
- export function asVideoId(value: string): VideoId {
- return value as VideoId;
- }
- export function asTagId(value: string): TagId {
- return value as TagId;
- }
- export function asCategoryId(value: string): CategoryId {
- return value as CategoryId;
- }
- export function asChannelId(value: string): ChannelId {
- return value as ChannelId;
- }
- /**
- * Redis cache key semantic classification.
- *
- * Use these constants to document the expected type and operations for a key.
- */
- export enum RedisKeySemanticType {
- /**
- * LIST of video IDs (strings only, no JSON).
- * Operations: LRANGE, RPUSH, LPUSH
- * Example: box:app:video:category:list:cat-1 → ['vid-1', 'vid-2', ...]
- */
- VideoIdList = 'VideoIdList',
- /**
- * LIST of video IDs with tag filter (strings only, no JSON).
- * Operations: LRANGE, RPUSH, LPUSH
- * Example: box:app:video:tag:list:cat-1:tag-1 → ['vid-1', 'vid-3', ...]
- */
- FilteredVideoIdList = 'FilteredVideoIdList',
- /**
- * ZSET of video IDs with timestamp scores for pagination.
- * Operations: ZREVRANGE, ZADD
- * Example: box:app:video:list:category:ch-1:cat-1:latest
- * Members: video IDs, Scores: timestamps
- */
- VideoIdZSet = 'VideoIdZSet',
- /**
- * LIST of Tag JSON objects.
- * Operations: LRANGE, RPUSH, LPUSH (parse each element as JSON)
- * Example: box:app:tag:list:cat-1 → ['{"id":"tag-1","name":"Sports",...}', ...]
- */
- TagJsonList = 'TagJsonList',
- /**
- * STRING containing JSON array of Tag objects.
- * Operations: GET, SET (parse result as JSON)
- * Example: box:app:tag:all → '[{"id":"tag-1","name":"Sports",...}, ...]'
- */
- TagJsonString = 'TagJsonString',
- /**
- * STRING containing JSON object (video detail, category, etc.)
- * Operations: GET, SET (parse result as JSON)
- * Example: box:app:video:detail:vid-1 → '{"id":"vid-1","title":"Video 1",...}'
- */
- DetailJson = 'DetailJson',
- /**
- * LIST of generic items (various uses).
- * Operations: LRANGE, RPUSH, LPUSH
- * Example: box:app:video:list:home:ch-1:latest → ['vid-1', 'vid-2', ...]
- */
- ItemList = 'ItemList',
- }
- /**
- * Redis command type for a given semantic type.
- *
- * Maps semantic types to their primary read/write operations.
- */
- export const RedisCommandsForSemanticType: Record<
- RedisKeySemanticType,
- { read: string[]; write: string[] }
- > = {
- [RedisKeySemanticType.VideoIdList]: {
- read: ['LRANGE'],
- write: ['DEL', 'RPUSH', 'LPUSH'],
- },
- [RedisKeySemanticType.FilteredVideoIdList]: {
- read: ['LRANGE'],
- write: ['DEL', 'RPUSH', 'LPUSH'],
- },
- [RedisKeySemanticType.VideoIdZSet]: {
- read: ['ZREVRANGE', 'ZRANGE'],
- write: ['DEL', 'ZADD'],
- },
- [RedisKeySemanticType.TagJsonList]: {
- read: ['LRANGE'],
- write: ['DEL', 'RPUSH', 'LPUSH'],
- },
- [RedisKeySemanticType.TagJsonString]: {
- read: ['GET'],
- write: ['SET', 'DEL'],
- },
- [RedisKeySemanticType.DetailJson]: {
- read: ['GET'],
- write: ['SET', 'DEL'],
- },
- [RedisKeySemanticType.ItemList]: {
- read: ['LRANGE'],
- write: ['DEL', 'RPUSH', 'LPUSH'],
- },
- };
- /**
- * Cache key metadata for documentation and validation.
- *
- * Define the semantics of each cache key pattern to enforce type safety
- * and prevent common mistakes.
- */
- export interface CacheKeyMetadata {
- /**
- * Human-readable description of the key.
- */
- description: string;
- /**
- * Semantic type (determines Redis type and operations).
- */
- semanticType: RedisKeySemanticType;
- /**
- * Example key (with placeholders like {categoryId}).
- */
- exampleKey: string;
- /**
- * Which builder creates/updates this key.
- */
- builtBy: string;
- /**
- * Which service/component reads this key.
- */
- readBy: string[];
- /**
- * Notes or special considerations.
- */
- notes?: string;
- }
- /**
- * Metadata registry for all major cache keys.
- *
- * Use this for validation, documentation generation, or runtime checks.
- */
- export const CacheKeyMetadataRegistry: Record<string, CacheKeyMetadata> = {
- 'app:video:category:list': {
- description: 'All video IDs in a category, in business order',
- semanticType: RedisKeySemanticType.VideoIdList,
- exampleKey: 'box:app:video:category:list:{categoryId}',
- builtBy: 'VideoCategoryCacheBuilder.buildCategoryListForChannel()',
- readBy: ['VideoService.getCategoryListForChannel()'],
- notes:
- 'LIST of video IDs only (strings), never JSON objects. Used by category detail API.',
- },
- 'app:video:tag:list': {
- description: 'Video IDs in category filtered by tag, in business order',
- semanticType: RedisKeySemanticType.FilteredVideoIdList,
- exampleKey: 'box:app:video:tag:list:{categoryId}:{tagId}',
- builtBy: 'VideoCategoryCacheBuilder.buildTagListForCategory()',
- readBy: ['VideoService.getTagListForCategory()'],
- notes:
- 'LIST of video IDs only (strings), subset of category videos. Distinct from tag.all().',
- },
- 'app:video:list:category': {
- description:
- 'Scored video IDs for a category (for pagination with consistent ordering)',
- semanticType: RedisKeySemanticType.VideoIdZSet,
- exampleKey: 'box:app:video:list:category:{channelId}:{categoryId}:{sort}',
- builtBy: 'VideoListCacheBuilder.buildCategoryPoolsForChannel()',
- readBy: ['VideoService.getVideosByCategoryWithPaging()'],
- notes:
- 'ZSET with video IDs as members and timestamp scores. Use ZREVRANGE for pagination.',
- },
- 'app:video:list:tag': {
- description: 'Scored video IDs for a tag (for pagination with ordering)',
- semanticType: RedisKeySemanticType.VideoIdZSet,
- exampleKey: 'box:app:video:list:tag:{channelId}:{tagId}:{sort}',
- builtBy: 'VideoListCacheBuilder.buildTagPoolsForChannel()',
- readBy: ['VideoService.getVideosByTagWithPaging()'],
- notes:
- 'ZSET with video IDs as members and timestamp scores. Use ZREVRANGE for pagination.',
- },
- 'app:video:list:home': {
- description: 'Home page section videos',
- semanticType: RedisKeySemanticType.ItemList,
- exampleKey: 'box:app:video:list:home:{channelId}:{section}',
- builtBy: 'VideoListCacheBuilder.buildHomeSectionsForChannel()',
- readBy: ['VideoService.getHomeSectionVideos()'],
- notes: 'LIST of video IDs, top N most recent across all categories.',
- },
- 'app:tag:all': {
- description: 'Global pool of all tags (for tag filter suggestions)',
- semanticType: RedisKeySemanticType.TagJsonString,
- exampleKey: 'box:app:tag:all',
- builtBy: 'TagCacheBuilder.buildAll()',
- readBy: ['TagCacheService.getAllTags()'],
- notes:
- 'JSON array of Tag objects. ✅ Contains TAG JSON, NOT video IDs. Distinct from video.tagList().',
- },
- 'app:video:detail': {
- description: 'Single video metadata/detail',
- semanticType: RedisKeySemanticType.DetailJson,
- exampleKey: 'box:app:video:detail:{videoId}',
- builtBy: 'VideoDetailCacheBuilder (if applicable)',
- readBy: ['VideoService.getVideoDetail()'],
- },
- };
- /**
- * Validation helper: Check if a key is expected to contain video IDs.
- *
- * @param keyPattern - The cache key pattern (e.g., 'app:video:category:list')
- * @returns true if the key contains video IDs (not JSON)
- */
- export function isVideoIdKey(keyPattern: string): boolean {
- const metadata = CacheKeyMetadataRegistry[keyPattern];
- if (!metadata) return false;
- return (
- metadata.semanticType === RedisKeySemanticType.VideoIdList ||
- metadata.semanticType === RedisKeySemanticType.FilteredVideoIdList ||
- metadata.semanticType === RedisKeySemanticType.VideoIdZSet
- );
- }
- /**
- * Validation helper: Check if a key is expected to contain JSON objects.
- *
- * @param keyPattern - The cache key pattern
- * @returns true if the key contains JSON (not raw strings)
- */
- export function isJsonKey(keyPattern: string): boolean {
- const metadata = CacheKeyMetadataRegistry[keyPattern];
- if (!metadata) return false;
- return (
- metadata.semanticType === RedisKeySemanticType.TagJsonList ||
- metadata.semanticType === RedisKeySemanticType.TagJsonString ||
- metadata.semanticType === RedisKeySemanticType.DetailJson
- );
- }
- /**
- * Validation helper: Get Redis operation types for a key pattern.
- *
- * @param keyPattern - The cache key pattern
- * @returns Object with read and write operations
- */
- export function getOperationsForKey(
- keyPattern: string,
- ): { read: string[]; write: string[] } | null {
- const metadata = CacheKeyMetadataRegistry[keyPattern];
- if (!metadata) return null;
- return RedisCommandsForSemanticType[metadata.semanticType];
- }
|