Browse Source

refactor(cache): standardize cache key format and clean up unused code

Dave 2 months ago
parent
commit
4e10392820

+ 1 - 1
apps/box-app-api/src/feature/channel/channel.service.ts

@@ -16,7 +16,7 @@ export class ChannelService {
       this.logger.log(`Cache miss for channel of channelId=${channelId}`);
 
       // Try to get from Redis cache first
-      const cacheKey = `channel:${channelId}`;
+      const cacheKey = `box:app:channel:id:${channelId}`;
       const cachedChannel = await this.redis.get(cacheKey);
       if (cachedChannel) {
         this.logger.log(`Cache hit for channelId: ${channelId}`);

+ 0 - 31
apps/box-mgnt-api/src/cache-sync/cache-sync.service.ts

@@ -68,10 +68,6 @@ export class CacheSyncService {
     return BigInt(Date.now());
   }
 
-  /**
-   * Build a safe error string that fits within database column constraints.
-   * Truncates if necessary to prevent P2000 errors.
-   */
   private buildLastErrorString(
     err: unknown,
     maxLength = this.MAX_LAST_ERROR_LENGTH,
@@ -101,10 +97,6 @@ export class CacheSyncService {
     return base;
   }
 
-  /**
-   * Enqueue a cache-sync action with optional initial delay.
-   * Downstream processing relies on attempts/nextAttemptAt for retries.
-   */
   async scheduleAction(params: {
     entityType: CacheEntityType;
     operation: CacheOperation;
@@ -156,10 +148,6 @@ export class CacheSyncService {
     );
   }
 
-  // ─────────────────────────────────────────────
-  // Convenience helpers — used by mgnt services or debug controller
-  // ─────────────────────────────────────────────
-
   async scheduleChannelRefreshAll(): Promise<void> {
     await this.scheduleAction({
       entityType: CacheEntityType.CHANNEL,
@@ -204,23 +192,11 @@ export class CacheSyncService {
     }
   }
 
-  // ─────────────────────────────────────────────
-  // Queue processing: cron + manual
-  // ─────────────────────────────────────────────
-
-  /**
-   * Cron job: process pending actions every 10 seconds.
-   */
   @Cron(CronExpression.EVERY_10_SECONDS)
   async processQueueCron() {
     await this.processPendingOnce(50);
   }
 
-  /**
-   * Pull a batch of pending actions (whose nextAttemptAt <= now) and process
-   * them with retry/backoff. Keeps PENDING actions in the queue until either
-   * success or we exhaust maxAttempts (then we mark GAVE_UP).
-   */
   async processPendingOnce(limit = 20): Promise<void> {
     const now = this.nowBigInt();
 
@@ -329,9 +305,6 @@ export class CacheSyncService {
     }
   }
 
-  /**
-   * Main dispatcher: decide what to do for each action.
-   */
   private async handleSingleAction(action: CacheSyncAction): Promise<void> {
     const handler = this.actionHandlers[action.entityType as CacheEntityType];
 
@@ -350,10 +323,6 @@ export class CacheSyncService {
     );
   }
 
-  /**
-   * Exponential backoff with light jitter so multiple workers don't retry in
-   * lockstep. Capped at 60s to avoid unbounded delays.
-   */
   private calculateBackoffMs(attempts: number): number {
     const jitter = Math.floor(Math.random() * 500);
     return Math.min(60000, this.baseBackoffMs * attempts + jitter);

+ 2 - 2
apps/box-mgnt-api/src/mgnt-backend/feature/redis-inspector/redis-cache-registry.ts

@@ -127,7 +127,7 @@ export const RedisCacheKeyAllowList: RedisCacheKeyAllowListEntry[] = [
   },
   {
     cacheCode: 'CATEGORY_BY_ID',
-    matcher: (key) => key.startsWith('box:app:category:by-id:'),
+    matcher: (key) => key.startsWith('box:app:category:id:'),
     description: 'Category detail key',
   },
   {
@@ -137,7 +137,7 @@ export const RedisCacheKeyAllowList: RedisCacheKeyAllowListEntry[] = [
   },
   {
     cacheCode: 'CHANNEL_BY_ID',
-    matcher: (key) => key.startsWith('box:app:channel:by-id:'),
+    matcher: (key) => key.startsWith('box:app:channel:id:'),
     description: 'Channel detail key',
   },
   {

+ 4 - 33
libs/common/src/cache/cache-keys.ts

@@ -12,57 +12,36 @@ export type VideoSortKey = 'latest' | 'popular' | 'manual';
 export type VideoHomeSectionKey = 'featured' | 'latest' | 'editorPick';
 
 export const CacheKeys = {
-  // ─────────────────────────────────────────────
-  // CHANNELS
-  // ─────────────────────────────────────────────
   appChannelAll: 'box:app:channel:all',
   appChannelById: (channelId: string | number): string =>
-    `box:app:channel:by-id:${channelId}`,
+    `box:app:channel:id:${channelId}`,
 
   appChannelWithCategories: (channelId: string | number): string =>
     `box:app:channel:with-categories:${channelId}`,
 
-  // ─────────────────────────────────────────────
-  // CATEGORIES
-  // ─────────────────────────────────────────────
   appCategory: (categoryId: string | number): string =>
     `box:app:category:${categoryId}`,
   appCategoryAll: 'box:app:category:all',
   appCategoryById: (categoryId: string | number): string =>
-    `box:app:category:by-id:${categoryId}`,
+    `box:app:category:id:${categoryId}`,
 
   appCategoryWithTags: (categoryId: string | number): string =>
     `box:app:category:with-tags:${categoryId}`,
 
-  // ─────────────────────────────────────────────
-  // TAGS
-  // ─────────────────────────────────────────────
   appTagAll: 'box:app:tag:all',
 
   appTagByCategoryKey: (categoryId: string | number): string =>
     `box:app:tag:list:${categoryId}`,
 
-  // ─────────────────────────────────────────────
-  // ADS
-  // ─────────────────────────────────────────────
-  // ─────────────────────────────────────────────
-  // AD POOLS (AdType-based)
-  // ─────────────────────────────────────────────
   appAdPoolByType: (adType: AdType | string): string =>
     `box:app:adpool:${adType}`,
 
-  // ─────────────────────────────────────────────
-  // VIDEO LISTS
-  // ─────────────────────────────────────────────
   appHomeVideoPage: (page: number): string =>
     `box:app:videolist:home:page:${page}`,
 
   appChannelVideoPage: (channelId: string | number, page: number): string =>
     `box:app:videolist:channel:${channelId}:page:${page}`,
 
-  // ─────────────────────────────────────────────
-  // VIDEO DETAILS & METADATA
-  // ─────────────────────────────────────────────
   appVideoDetailKey: (videoId: string): string =>
     `box:app:video:detail:${videoId}`,
 
@@ -75,9 +54,6 @@ export const CacheKeys = {
   appVideoTagListKey: (categoryId: string, tagId: string): string =>
     `box:app:video:tag:list:${categoryId}:${tagId}`,
 
-  // ─────────────────────────────────────────────
-  // VIDEO POOLS
-  // ─────────────────────────────────────────────
   appVideoCategoryPoolKey: (
     channelId: string,
     categoryId: string,
@@ -90,14 +66,9 @@ export const CacheKeys = {
     sort: VideoSortKey,
   ): string => `box:app:video:list:tag:${channelId}:${tagId}:${sort}`,
 
-  appVideoHomeSectionKey: (
-    channelId: string,
-    section: string,
-  ): string => `box:app:video:list:home:${channelId}:${section}`,
+  appVideoHomeSectionKey: (channelId: string, section: string): string =>
+    `box:app:video:list:home:${channelId}:${section}`,
 
-  // ─────────────────────────────────────────────
-  // RECOMMENDED VIDEOS
-  // ─────────────────────────────────────────────
   appRecommendedVideos: 'box:app:video:recommended',
   appVideoLatest: 'box:app:video:latest',
   appVideoList: 'box:app:video:list',

+ 0 - 118
libs/common/src/cache/ts-cache-key.provider.ts

@@ -49,9 +49,6 @@ export interface TsCacheKeyBuilder {
     withCategories(channelId: string | number): string;
   };
 
-  /**
-   * Category-related cache keys.
-   */
   category: {
     /** Get category by ID. */
     byId(categoryId: string | number): string;
@@ -63,43 +60,8 @@ export interface TsCacheKeyBuilder {
     withTags(categoryId: string | number): string;
   };
 
-  /**
-   * Tag-related cache keys.
-   *
-   * ⚠️ CRITICAL: Distinguish between tag.all() (a global TAG POOL that contains Tag JSON objects)
-   * and video.tagList() (which returns video IDs filtered by tag and contains VIDEO IDs only).
-   *
-   * SEE: libs/common/src/cache/CACHE_SEMANTICS.md
-   */
   tag: {
-    /**
-     * Get all tags (global suggestion pool).
-     * Redis Type: JSON (Array of Tag objects)
-     * Elements: Tag objects (stringified JSON)
-     *
-     * Format: { id, name, seq, status, createAt, updateAt, channelId, categoryId }
-     *
-     * ✅ This key contains TAG JSON objects
-     * ❌ Not video IDs (that's video.tagList)
-     *
-     * Example: tsCacheKeys.tag.all()
-     *          → "box:app:tag:all"
-     *          → GET and parse as JSON array
-     */
     all(): string;
-
-    /**
-     * Get tag metadata (JSON array) for a specific category.
-     * Redis Type: STRING (JSON array)
-     * Elements: Tag metadata objects with id, name, seq, etc.
-     *
-     * ✅ This key contains TAG METADATA (entire tag objects)
-     * ❌ Not video IDs (that's video.tagList)
-     *
-     * Example: tsCacheKeys.tag.metadataByCategory('cat-001')
-     *          → "box:app:tag:list:cat-001"
-     *          → GET and parse as JSON array
-     */
     metadataByCategory(categoryId: string | number): string;
   };
 
@@ -111,89 +73,17 @@ export interface TsCacheKeyBuilder {
     poolByType(adType: AdType | string): string;
   };
 
-  /**
-   * Video-related cache keys.
-   *
-   * ⚠️ CRITICAL: Understand the distinction between "list" and "pool" keys.
-   *
-   * LIST keys (video.categoryList, video.tagList) map to Redis LISTs that store video IDs only (strings like "64a2b3c4d5e6f7g8h9i0j1k2") in business sequence order and operate via LRANGE/RPUSH to return videos for categories or tags.
-   *
-   * POOL keys (video.categoryPool, video.tagPool) map to Redis ZSETs whose members are video IDs with timestamp scores sorted from newest to oldest; they rely on ZREVRANGE/ZADD for pagination.
-   *
-   * SEE: libs/common/src/cache/CACHE_SEMANTICS.md for complete documentation
-   */
   video: {
-    /**
-     * Get video detail data.
-     * Redis Type: STRING (JSON object)
-     */
     detail(videoId: string): string;
-    /**
-     * Get video payload data (minimal metadata) per video.
-     * Redis Type: STRING (JSON object)
-     */
     payload(videoId: string): string;
-
-    /**
-     * Get video category list by category.
-     * Redis Type: LIST
-     * Elements: Video IDs ONLY (strings)
-     * Order: Business sequence (seq/newest first)
-     *
-     * Example: tsCacheKeys.video.categoryList('cat-001')
-     *          → "box:app:video:category:list:cat-001"
-     */
     categoryList(categoryId: string): string;
-
-    /**
-     * Get video tag list by category and tag.
-     * Redis Type: LIST
-     * Elements: Video IDs ONLY (strings, filtered by tag)
-     * Order: Same as categoryList
-     *
-     * ⚠️ NOT to be confused with tag.all() which contains TAG JSON
-     *
-     * Example: tsCacheKeys.video.tagList('cat-001', 'tag-sports')
-     *          → "box:app:video:tag:list:cat-001:tag-sports"
-     */
     tagList(categoryId: string, tagId: string): string;
-
-    /**
-     * Get videos in a category with sort order (ZSET pool).
-     * Redis Type: ZSET
-     * Members: Video IDs (strings)
-     * Scores: Timestamp (newest first via ZREVRANGE)
-     * Use: Pagination with consistent ordering
-     *
-     * Example: tsCacheKeys.video.categoryPool('ch-1', 'cat-1', 'latest')
-     *          → "box:app:video:list:category:ch-1:cat-1:latest"
-     *          → ZREVRANGE to get page results
-     */
     categoryPool(
       channelId: string,
       categoryId: string,
       sort: VideoSortKey,
     ): string;
-
-    /**
-     * Get videos with a specific tag with sort order (ZSET pool).
-     * Redis Type: ZSET
-     * Members: Video IDs (strings)
-     * Scores: Timestamp (newest first via ZREVRANGE)
-     * Use: Pagination with consistent ordering
-     *
-     * Example: tsCacheKeys.video.tagPool('ch-1', 'tag-1', 'latest')
-     *          → "box:app:video:list:tag:ch-1:tag-1:latest"
-     *          → ZREVRANGE to get page results
-     */
     tagPool(channelId: string, tagId: string, sort: VideoSortKey): string;
-
-    /**
-     * Get home page video section.
-     * Redis Type: LIST
-     * Elements: Video IDs (strings)
-     * Order: Most recent first (limited to N items)
-     */
     homeSection(channelId: string, section: string): string;
     recommended(): string;
     latest(): string;
@@ -214,10 +104,6 @@ export interface TsCacheKeyBuilder {
   };
 }
 
-/**
- * Create a TypeScript-friendly cache key builder.
- * This provides structured access to all cache key generators with proper typing.
- */
 export function createTsCacheKeyBuilder(): TsCacheKeyBuilder {
   return {
     channel: {
@@ -267,8 +153,4 @@ export function createTsCacheKeyBuilder(): TsCacheKeyBuilder {
     },
   };
 }
-
-/**
- * Singleton instance of the TypeScript-friendly cache key builder.
- */
 export const tsCacheKeys = createTsCacheKeyBuilder();

+ 0 - 112
libs/common/src/cache/video-cache.helper.ts

@@ -99,27 +99,6 @@ export class VideoCacheHelper {
 
   constructor(private readonly redis: RedisService) {}
 
-  /**
-   * Save a list of video IDs to a Redis LIST key.
-   *
-   * REDIS OPERATIONS: delete the existing key, push all IDs with RPUSH, and optionally set TTL if requested.
-   *
-   * KEY TYPE: LIST, holding video IDs as 24-character hex strings (MongoDB ObjectId format).
-   * VALUE TYPE: the video IDs themselves.
-   *
-   * @param key - Redis key (e.g., 'box:app:video:category:list:{categoryId}')
-   * @param videoIds - Array of video IDs to store
-   * @param ttlSeconds - Optional TTL in seconds (no expiration if omitted)
-   *
-   * @throws Error if Redis operations fail
-   *
-   * Example call:
-   * await helper.saveVideoIdList(
-   *   'box:app:video:category:list:abc123',
-   *   ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012'],
-   *   3600
-   * );
-   */
   async saveVideoIdList(
     key: string,
     videoIds: string[],
@@ -155,25 +134,6 @@ export class VideoCacheHelper {
     }
   }
 
-  /**
-   * Get a range of video IDs from a Redis LIST key.
-   *
-   * REDIS OPERATION: LRANGE key start stop
-   * KEY TYPE: LIST
-   * VALUE TYPE: Video IDs (24-character hex strings)
-   *
-   * @param key - Redis key (e.g., 'box:app:video:category:list:{categoryId}')
-   * @param start - Start index (0-based, default 0)
-   * @param stop - Stop index (-1 for all, default -1)
-   * @returns Array of video IDs
-   *
-   * @example
-   * // Get all video IDs
-   * const allIds = await helper.getVideoIdList('box:app:video:category:list:abc123');
-   *
-   * // Get first 20 video IDs (pagination)
-   * const pageIds = await helper.getVideoIdList('box:app:video:category:list:abc123', 0, 19);
-   */
   async getVideoIdList(key: string, start = 0, stop = -1): Promise<string[]> {
     try {
       const videoIds = await this.readListRangeWithLegacy(
@@ -194,18 +154,6 @@ export class VideoCacheHelper {
     }
   }
 
-  /**
-   * Get the length of a video ID list.
-   *
-   * REDIS OPERATION: LLEN key
-   * KEY TYPE: LIST
-   *
-   * @param key - Redis key
-   * @returns Number of items in the list
-   *
-   * @example
-   * const count = await helper.getVideoIdListLength('box:app:video:category:list:abc123');
-   */
   async getVideoIdListLength(key: string): Promise<number> {
     try {
       const length = await this.redis.llen(key);
@@ -230,27 +178,6 @@ export class VideoCacheHelper {
     }
   }
 
-  /**
-   * Save tag metadata list to Redis.
-   *
-   * REDIS OPERATIONS:
-   * 1. DEL key
-   * 2. RPUSH key (JSON.stringify(tag1)) (JSON.stringify(tag2)) ...
-   * 3. EXPIRE key ttlSeconds (if provided)
-   *
-   * KEY TYPE: LIST
-   * VALUE TYPE: Tag JSON objects (stringified)
-   *
-   * @param key - Redis key (e.g., 'box:app:tag:list:{categoryId}')
-   * @param tags - Array of tag metadata objects
-   * @param ttlSeconds - Optional TTL in seconds
-   *
-   * @example
-   * await helper.saveTagList('box:app:tag:list:abc123', [
-   *   { id: '1', name: 'Action', seq: 1, ... },
-   *   { id: '2', name: 'Drama', seq: 2, ... }
-   * ], 3600);
-   */
   async saveTagList(
     key: string,
     tags: TagMetadata[],
@@ -287,23 +214,6 @@ export class VideoCacheHelper {
     }
   }
 
-  /**
-   * Get tag metadata list for a category.
-   *
-   * REDIS OPERATION: LRANGE key 0 -1
-   * KEY TYPE: LIST
-   * VALUE TYPE: Tag JSON objects (stringified)
-   *
-   * Parses each JSON string into a TagMetadata object.
-   * Skips items that fail to parse (logs warning).
-   *
-   * @param key - Redis key (e.g., 'box:app:tag:list:{categoryId}')
-   * @returns Array of parsed tag metadata objects
-   *
-   * @example
-   * const tags = await helper.getTagListForCategory('box:app:tag:list:abc123');
-   * // Returns: [{ id: '1', name: 'Action', ... }, { id: '2', name: 'Drama', ... }]
-   */
   async getTagListForCategory(key: string): Promise<TagMetadata[]> {
     try {
       const items = await this.readListRangeWithLegacy(
@@ -328,17 +238,6 @@ export class VideoCacheHelper {
     }
   }
 
-  /**
-   * Check if a key exists in Redis.
-   *
-   * REDIS OPERATION: EXISTS key
-   *
-   * @param key - Redis key to check
-   * @returns true if key exists, false otherwise
-   *
-   * @example
-   * const exists = await helper.keyExists('box:app:video:category:list:abc123');
-   */
   async keyExists(key: string): Promise<boolean> {
     try {
       const result = await this.redis.exists(key);
@@ -352,17 +251,6 @@ export class VideoCacheHelper {
     }
   }
 
-  /**
-   * Delete a key from Redis.
-   *
-   * REDIS OPERATION: DEL key
-   *
-   * @param key - Redis key to delete
-   * @returns Number of keys deleted (0 or 1)
-   *
-   * @example
-   * await helper.deleteKey('box:app:video:category:list:abc123');
-   */
   async deleteKey(key: string): Promise<number> {
     try {
       return await this.redis.del(key);

+ 7 - 7
libs/core/src/cache/channel/channel-cache.builder.ts

@@ -5,15 +5,15 @@ import { RedisService } from '@box/db/redis/redis.service';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
 
 export interface ChannelCachePayload {
-  id: string;
+  // id: string;
   channelId: string;
-  name: string;
+  // name: string;
   landingUrl: string;
   videoCdn?: string;
   coverCdn?: string;
   clientName?: string;
   clientNotice?: string;
-  remark?: string;
+  // remark?: string;
 }
 
 @Injectable()
@@ -28,20 +28,20 @@ export class ChannelCacheBuilder extends BaseCacheBuilder {
     });
 
     const payloads: ChannelCachePayload[] = channels.map((channel) => ({
-      id: channel.id,
+      // id: channel.id,
       channelId: channel.channelId,
-      name: channel.name,
+      // name: channel.name,
       landingUrl: channel.landingUrl,
       videoCdn: channel.videoCdn ?? undefined,
       coverCdn: channel.coverCdn ?? undefined,
       clientName: channel.clientName ?? undefined,
       clientNotice: channel.clientNotice ?? undefined,
-      remark: channel.remark ?? undefined,
+      // remark: channel.remark ?? undefined,
     }));
 
     const entries: Array<{ key: string; value: unknown }> = payloads.map(
       (payload) => ({
-        key: tsCacheKeys.channel.byId(payload.id),
+        key: tsCacheKeys.channel.byId(payload.channelId),
         value: payload,
       }),
     );

+ 13 - 4
libs/core/src/cache/channel/channel-cache.service.ts

@@ -17,11 +17,20 @@ export class ChannelCacheService extends BaseCacheService {
     );
   }
 
-  async getChannelById(id: string): Promise<ChannelCachePayload | null> {
-    if (!id) return null;
+  // async getChannelById(id: string): Promise<ChannelCachePayload | null> {
+  //   if (!id) return null;
+  //   return (
+  //     (await this.getJson<ChannelCachePayload>(tsCacheKeys.channel.byId(id))) ??
+  //     null
+  //   );
+  // }
+
+  async getChannelById(channelId: string): Promise<ChannelCachePayload | null> {
+    if (!channelId) return null;
     return (
-      (await this.getJson<ChannelCachePayload>(tsCacheKeys.channel.byId(id))) ??
-      null
+      (await this.getJson<ChannelCachePayload>(
+        tsCacheKeys.channel.byId(channelId),
+      )) ?? null
     );
   }
 }

+ 0 - 10
libs/core/src/cache/video/list/video-list-cache.builder.ts

@@ -25,16 +25,6 @@ export class VideoListCacheBuilder extends BaseCacheBuilder {
     super(redis, mongoPrisma, VideoListCacheBuilder.name);
   }
 
-  /**
-   * Build all video pools and home sections for all channels.
-   *
-   * Process:
-   *  1. Fetch all channels
-   *  2. For each channel:
-   *     - Build category pools (ZSET per category)
-   *     - Build tag pools (ZSET per tag) - TODO: Refactor after schema change
-   *     - Build home sections (LIST per section) - TODO: Refactor after schema change
-   */
   async buildAll(): Promise<void> {
     const channels = await this.mongoPrisma.channel.findMany();
 

+ 1 - 8
libs/core/src/cache/video/recommended/recommended-videos-cache.builder.ts

@@ -10,14 +10,7 @@ import {
 } from '../video-item-mapper';
 
 export type { RecommendedVideoItem } from '../video-item-mapper';
-/**
- * Cache builder for recommended videos (homepage).
- * Builds a Redis string (JSON array) containing 99 random completed videos.
- *
- * Cache key: app:video:recommended
- * Structure: JSON array of RecommendedVideoItem[]
- * TTL: Can be refreshed periodically or on-demand
- */
+
 @Injectable()
 export class RecommendedVideosCacheBuilder extends BaseCacheBuilder {
   private readonly RECOMMENDED_COUNT = 99;