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.
libs/core/src/cache/video/video-list-cache.builder.ts (248 lines)
@Injectable()
export class VideoListCacheBuilder extends BaseCacheBuilder {
async buildAll(): Promise<void>;
private buildCategoryPoolsForChannel(channelId: string): Promise<void>;
private buildTagPoolsForChannel(channelId: string): Promise<void>;
private buildHomeSectionsForChannel(channelId: string): Promise<void>;
private getVideoScore(video: any): number;
}
tsCacheKeys.video.categoryPool(channelId, categoryId, 'latest')editedAt (local edit time), falls back to updatedAtlistStatus === 1 (on shelf)tsCacheKeys.video.tagPool(channelId, tagId, 'latest')tsCacheKeys.video.homeSection(channelId, 'featured'), 'latest', 'editorPick'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
Uses the existing VideoMedia model from Prisma MongoDB:
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:
categoryIdtagIds arrayCategory.channelIdTag.channelIdprivate 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;
}
libs/core/src/cache/video/index.ts
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
VideoListCacheBuilderVideoListCacheBuilderVideoListCacheBuilderlibs/core/src/cache/index.ts (unchanged)
export * from './video'; // Includes VideoListCacheBuilder
export * from './category';
export * from './tag';
export * from './channel';
The builder can now be imported via clean paths:
// Preferred: explicit path
import { VideoListCacheBuilder } from '@box/core/cache/video';
// Alternative: generic cache import
import { VideoListCacheBuilder } from '@box/core/cache';
✅ Typecheck: PASS (0 errors) ✅ Lint: PASS (0 warnings) ✅ Build: PASS (box-mgnt-api compiled successfully)
The builder is already registered in CacheManagerModule, making it available for injection:
// In apps/box-mgnt-api/src/cache/video-list-warmup.service.ts (when wired)
constructor(
private readonly videoListBuilder: VideoListCacheBuilder,
) {}
async onModuleInit(): Promise<void> {
await this.videoListBuilder.buildAll();
}
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 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
The MVP implementation treats all home sections identically. To customize:
private async buildHomeSectionsForChannel(channelId: string): Promise<void> {
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
}
libs/common/src/cache/cache-keys.tslibs/db/src/redis/redis.service.tslibs/common/src/cache/cache-builder.tslibs/core/src/cache/video/video-category-cache.builder.ts