# Core VideoListCacheBuilder Implementation ## Summary Successfully implemented a core `VideoListCacheBuilder` in `libs/core/src/cache/video/` that builds Redis ZSET pools for video lists by category and tag, plus LIST pools for home page sections per channel. The builder follows the `BaseCacheBuilder` pattern and integrates cleanly into the cache manager module. ## Implementation Details ### File Created **libs/core/src/cache/video/video-list-cache.builder.ts** (248 lines) ```typescript @Injectable() export class VideoListCacheBuilder extends BaseCacheBuilder { async buildAll(): Promise; private buildCategoryPoolsForChannel(channelId: string): Promise; private buildTagPoolsForChannel(channelId: string): Promise; private buildHomeSectionsForChannel(channelId: string): Promise; private getVideoScore(video: any): number; } ``` ### Key Features #### 1. Category Pools (ZSET) - **Key**: `tsCacheKeys.video.categoryPool(channelId, categoryId, 'latest')` - **Type**: ZSET with videoId as member, score as timestamp - **Score**: Prefers `editedAt` (local edit time), falls back to `updatedAt` - **Filter**: Only includes videos where `listStatus === 1` (on shelf) - **Order**: Descending by score (most recent first) #### 2. Tag Pools (ZSET) - **Key**: `tsCacheKeys.video.tagPool(channelId, tagId, 'latest')` - **Type**: ZSET with videoId as member, score as timestamp - **Score**: Same logic as category pools (editedAt → updatedAt) - **Multi-tag Support**: Videos with multiple tags appear in multiple ZSET pools - **Order**: Descending by score (most recent first) #### 3. Home Sections (LIST) - **Keys**: `tsCacheKeys.video.homeSection(channelId, 'featured')`, `'latest'`, `'editorPick'` - **Type**: LIST of videoIds - **Content**: Top 50 most recent videos across all categories in the channel - **Order**: Descending by editedAt/updatedAt (most recent first via RPUSH) - **MVP Strategy**: All three sections return the same data (easy to customize later) ### Build Process ``` buildAll() ├─ For each channel: │ ├─ buildCategoryPoolsForChannel() │ │ └─ For each category in channel: │ │ ├─ Fetch all on-shelf videos (listStatus === 1) │ │ ├─ Sort by editedAt desc │ │ └─ ZADD to Redis ZSET │ │ │ ├─ buildTagPoolsForChannel() │ │ └─ For each tag in channel: │ │ ├─ Fetch all on-shelf videos with this tag (tagIds array contains tag) │ │ ├─ Sort by editedAt desc │ │ └─ ZADD to Redis ZSET │ │ │ └─ buildHomeSectionsForChannel() │ ├─ Fetch all on-shelf videos for all categories │ ├─ Sort by editedAt desc, take top 50 │ └─ For each section (featured, latest, editorPick): │ └─ RPUSH videoIds to Redis LIST │ └─ Log completion ``` ### Data Model Uses the existing `VideoMedia` model from Prisma MongoDB: ```typescript model VideoMedia { id: string // Mongo ObjectId title: string categoryId?: string | null // Linked category tagIds: string[] // Array of tag IDs (max 5) listStatus: int // 0: off shelf, 1: on shelf editedAt: BigInt // Local edit time (epoch ms) updatedAt: DateTime // Provider update time // ... other fields } ``` Relationships: - Videos link to Category via `categoryId` - Videos link to Tags via `tagIds` array - Category links to Channel via `Category.channelId` - Tag links to Channel via `Tag.channelId` ### Score Calculation ```typescript private getVideoScore(video: any): number { // Prefer editedAt (local edit time) if set and non-zero if (video.editedAt) { const ms = this.toMillis(video.editedAt); if (ms && ms > 0) return ms; } // Fall back to updatedAt (provider update time) return this.toMillis(video.updatedAt) ?? 0; } ``` - Converts BigInt to milliseconds - Enables consistent sorting across videos - Allows manual reordering via editedAt field ## Export Structure ### Updated Files **libs/core/src/cache/video/index.ts** ```typescript export { VideoCategoryCacheBuilder, VideoCategoryPayload, VideoTagPayload, } from './video-category-cache.builder'; export { VideoCategoryWarmupService } from './video-category-warmup.service'; export { VideoListCacheBuilder, VideoPoolPayload, } from './video-list-cache.builder'; ``` **libs/core/src/cache/cache-manager.module.ts** - Added import: `VideoListCacheBuilder` - Added to providers array: `VideoListCacheBuilder` - Added to exports array: `VideoListCacheBuilder` **libs/core/src/cache/index.ts** (unchanged) ```typescript export * from './video'; // Includes VideoListCacheBuilder export * from './category'; export * from './tag'; export * from './channel'; ``` ## Import Paths The builder can now be imported via clean paths: ```typescript // Preferred: explicit path import { VideoListCacheBuilder } from '@box/core/cache/video'; // Alternative: generic cache import import { VideoListCacheBuilder } from '@box/core/cache'; ``` ## Test Results ✅ **Typecheck**: PASS (0 errors) ✅ **Lint**: PASS (0 warnings) ✅ **Build**: PASS (box-mgnt-api compiled successfully) ## Integration with App-Level Warmup The builder is already registered in `CacheManagerModule`, making it available for injection: ```typescript // In apps/box-mgnt-api/src/cache/video-list-warmup.service.ts (when wired) constructor( private readonly videoListBuilder: VideoListCacheBuilder, ) {} async onModuleInit(): Promise { await this.videoListBuilder.buildAll(); } ``` ## Architecture ``` libs/core/src/cache/video/ ├── video-category-cache.builder.ts (Categories + Tags lists) ├── video-category-warmup.service.ts (Warmup orchestrator) ├── video-list-cache.builder.ts (NEW: Pools + Home sections) └── index.ts (Exports all) CacheManagerModule ├── Imports all builders ├── Registers in providers └── Exports for app-level use apps/box-mgnt-api/src/cache/ ├── video-category-warmup.service.ts (Uses core VideoCategoryCacheBuilder) ├── video-list-warmup.service.ts (Can use core VideoListCacheBuilder) └── video-list-cache.builder.ts (App-specific implementation) ``` ## No Duplication ✓ No duplicate `VideoListCacheBuilder` under `apps/box-mgnt-api` ✓ All video list cache logic centralized in core ✓ App-level builders are for app-specific needs (e.g., app-level video-list-cache.builder.ts) ✓ Core builder provides the canonical implementation ## Future Customization The MVP implementation treats all home sections identically. To customize: ```typescript private async buildHomeSectionsForChannel(channelId: string): Promise { const categories = await this.mongoPrisma.category.findMany({ /* ... */ }); const videos = await this.mongoPrisma.videoMedia.findMany({ /* ... */ }); // Customize per section: const featured = videos.filter(v => /* featured logic */); const latest = videos.filter(v => /* latest logic */); const editorPick = videos.filter(v => /* editor pick logic */); await this.redis.rpushList( tsCacheKeys.video.homeSection(channelId, 'featured'), featured.map(v => v.id), ); // ... similar for other sections } ``` ## Performance Considerations - **Category pools**: N categories × average videos per category = O(N) Redis operations - **Tag pools**: M tags × average videos per tag = O(M) Redis operations - **Home sections**: 3 sections × 1 Redis operation = O(3) Redis operations - **Total**: O(N + M + 3) per channel - **Optimization**: Could batch ZADD/RPUSH operations using Redis pipelines if needed ## References - **Cache keys**: `libs/common/src/cache/cache-keys.ts` - **Redis service**: `libs/db/src/redis/redis.service.ts` - **Base builder**: `libs/common/src/cache/cache-builder.ts` - **Related builder**: `libs/core/src/cache/video/video-category-cache.builder.ts`