VIDEO_LIST_CACHE_BUILDER.md 7.9 KB

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)

@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;
}

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:

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

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

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)

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:

// 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:

// 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();
}

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:

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
}

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