Преглед изворни кода

feat: enhance video caching with new payload handling and Redis key management

Dave пре 1 месец
родитељ
комит
5af388fddb

+ 2 - 0
.gitignore

@@ -1,6 +1,8 @@
 # compiled output
 /dist
 /node_modules
+action-plans/
+docs/
 
 # Logs
 logs

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

@@ -7,6 +7,13 @@ import type { VideoHomeSectionKey } from '@box/common/cache/ts-cache-key.provide
 import { VideoCacheHelper } from '@box/common/cache/video-cache.helper';
 import { CacheKeys } from '@box/common/cache/cache-keys';
 import {
+  RawVideoPayloadRow,
+  toVideoPayload,
+  VideoPayload,
+  videoCacheKeys,
+  parseVideoPayload,
+} from '@box/common/cache/video-cache.utils';
+import {
   VideoCategoryDto,
   VideoTagDto,
   VideoDetailDto,
@@ -689,8 +696,86 @@ export class VideoService {
         `Error fetching video details from DB`,
         err instanceof Error ? err.stack : String(err),
       );
-      return videoIds.map(() => null);
+    return videoIds.map(() => null);
+  }
+
+  private async getVideoPayloadsByIds(
+    videoIds: string[],
+  ): Promise<VideoPayload[]> {
+    if (!videoIds || videoIds.length === 0) {
+      return [];
     }
+
+    try {
+      const keys = videoIds.map((id) => videoCacheKeys.videoPayloadKey(id));
+      const cached = await this.redis.mget(keys);
+
+      const payloadMap = new Map<string, VideoPayload>();
+      const missing = new Set<string>();
+
+      cached.forEach((raw, idx) => {
+        const id = videoIds[idx];
+        if (!raw) {
+          missing.add(id);
+          return;
+        }
+
+        const parsed = parseVideoPayload(raw);
+        if (!parsed) {
+          missing.add(id);
+          return;
+        }
+
+        payloadMap.set(id, parsed);
+      });
+
+      if (missing.size > 0) {
+        const records = await this.mongoPrisma.videoMedia.findMany({
+          where: { id: { in: Array.from(missing) } },
+          select: {
+            id: true,
+            title: true,
+            coverImg: true,
+            coverImgNew: true,
+            videoTime: true,
+            country: true,
+            firstTag: true,
+            secondTags: true,
+            preFileName: true,
+            desc: true,
+            size: true,
+            updatedAt: true,
+            filename: true,
+            fieldNameFs: true,
+            ext: true,
+          },
+        });
+
+        if (records.length > 0) {
+          const pipelineEntries = records.map((row: RawVideoPayloadRow) => ({
+            key: videoCacheKeys.videoPayloadKey(row.id),
+            value: toVideoPayload(row),
+          }));
+
+          await this.redis.pipelineSetJson(pipelineEntries);
+          for (const row of records) {
+            payloadMap.set(row.id, toVideoPayload(row));
+            missing.delete(row.id);
+          }
+        }
+      }
+
+      return videoIds
+        .map((id) => payloadMap.get(id))
+        .filter((payload): payload is VideoPayload => Boolean(payload));
+    } catch (err) {
+      this.logger.error(
+        `Error fetching video payloads for ids=${videoIds.join(',')}`,
+        err instanceof Error ? err.stack : String(err),
+      );
+      return [];
+    }
+  }
   }
 
   /**

+ 77 - 0
libs/common/src/cache/video-cache.utils.ts

@@ -0,0 +1,77 @@
+// libs/common/src/cache/video-cache.utils.ts
+/** 
+ * Redis key helpers for video cache entries keyed by the canonical `box:app:*` namespace.
+ */
+export const videoCacheKeys = {
+  videoCategoryListKey: (categoryId: string): string =>
+    `box:app:video:category:list:${categoryId}`,
+  videoTagListKey: (categoryId: string, tagId: string): string =>
+    `box:app:video:tag:list:${categoryId}:${tagId}`,
+  videoPayloadKey: (videoId: string): string =>
+    `box:app:video:payload:${videoId}`,
+};
+
+export interface VideoPayload {
+  id: string;
+  title: string;
+  coverImg: string;
+  coverImgNew: string;
+  videoTime: number;
+  country: string;
+  firstTag: string;
+  secondTags: string[];
+  preFileName: string;
+  desc: string;
+  size: string;
+  updatedAt: string;
+  filename: string;
+  fieldNameFs: string;
+  ext: string;
+}
+
+export interface RawVideoPayloadRow {
+  id: string;
+  title: string;
+  coverImg: string;
+  coverImgNew: string;
+  videoTime: number;
+  country: string;
+  firstTag: string;
+  secondTags: string[];
+  preFileName: string;
+  desc: string;
+  size: bigint;
+  updatedAt: Date;
+  filename: string;
+  fieldNameFs: string;
+  ext: string;
+}
+
+export function toVideoPayload(row: RawVideoPayloadRow): VideoPayload {
+  return {
+    id: row.id,
+    title: row.title ?? '',
+    coverImg: row.coverImg ?? '',
+    coverImgNew: row.coverImgNew ?? '',
+    videoTime: row.videoTime ?? 0,
+    country: row.country ?? '',
+    firstTag: row.firstTag ?? '',
+    secondTags: Array.isArray(row.secondTags) ? row.secondTags : [],
+    preFileName: row.preFileName ?? '',
+    desc: row.desc ?? '',
+    size: row.size?.toString() ?? '0',
+    updatedAt: row.updatedAt?.toISOString() ?? new Date().toISOString(),
+    filename: row.filename ?? '',
+    fieldNameFs: row.fieldNameFs ?? '',
+    ext: row.ext ?? '',
+  };
+}
+
+export function parseVideoPayload(value: string | null): VideoPayload | null {
+  if (!value) return null;
+  try {
+    return JSON.parse(value) as VideoPayload;
+  } catch {
+    return null;
+  }
+}

+ 10 - 43
libs/core/src/cache/video/category/video-category-cache.builder.ts

@@ -4,8 +4,13 @@ import { BaseCacheBuilder } from '@box/common/cache/cache-builder';
 import { RedisService } from '@box/db/redis/redis.service';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
 import { CacheKeys } from '@box/common/cache/cache-keys';
+import { CacheKeys } from '@box/common/cache/cache-keys';
 import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import {
+  RawVideoPayloadRow,
+  toVideoPayload,
+} from '@box/common/cache/video-cache.utils';
+import {
   VideoCacheHelper,
   type TagMetadata,
 } from '@box/common/cache/video-cache.helper';
@@ -16,24 +21,6 @@ import {
  */
 export interface TagMetadataPayload extends TagMetadata {}
 
-interface VideoPayloadRow {
-  id: string;
-  title: string;
-  coverImg: string;
-  coverImgNew: string;
-  videoTime: number;
-  country: string;
-  firstTag: string;
-  secondTags: string[];
-  preFileName: string;
-  desc: string;
-  size: bigint;
-  updatedAt: Date;
-  filename: string;
-  fieldNameFs: string;
-  ext: string;
-}
-
 /**
  * Cache builder for video category/tag lists following new semantics.
  *
@@ -443,7 +430,7 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
 
   private async fetchVideosForCategory(
     categoryId: string,
-  ): Promise<VideoPayloadRow[]> {
+  ): Promise<RawVideoPayloadRow[]> {
     return this.mongoPrisma.videoMedia.findMany({
       where: {
         categoryIds: { has: categoryId },
@@ -473,7 +460,7 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
   private async fetchVideosForTag(
     categoryId: string,
     tagId: string,
-  ): Promise<VideoPayloadRow[]> {
+  ): Promise<RawVideoPayloadRow[]> {
     return this.mongoPrisma.videoMedia.findMany({
       where: {
         categoryIds: { has: categoryId },
@@ -501,7 +488,7 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
     });
   }
 
-  private async fetchVideoById(videoId: string): Promise<VideoPayloadRow | null> {
+  private async fetchVideoById(videoId: string): Promise<RawVideoPayloadRow | null> {
     return this.mongoPrisma.videoMedia.findUnique({
       where: { id: videoId },
       select: {
@@ -524,34 +511,14 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
     });
   }
 
-  private async writeVideoPayloads(videos: VideoPayloadRow[]): Promise<void> {
+  private async writeVideoPayloads(videos: RawVideoPayloadRow[]): Promise<void> {
     if (!videos.length) return;
 
     const entries = videos.map((video) => ({
       key: CacheKeys.appVideoPayloadKey(video.id),
-      value: this.mapToPayload(video),
+      value: toVideoPayload(video),
     }));
 
     await this.redis.pipelineSetJson(entries);
   }
-
-  private mapToPayload(video: VideoPayloadRow) {
-    return {
-      id: video.id,
-      title: video.title,
-      coverImg: video.coverImg,
-      coverImgNew: video.coverImgNew,
-      videoTime: video.videoTime,
-      country: video.country,
-      firstTag: video.firstTag,
-      secondTags: video.secondTags,
-      preFileName: video.preFileName,
-      desc: video.desc,
-      size: video.size,
-      updatedAt: video.updatedAt.toISOString(),
-      filename: video.filename,
-      fieldNameFs: video.fieldNameFs,
-      ext: video.ext,
-    };
-  }
 }

+ 6 - 0
libs/db/src/redis/redis.service.ts

@@ -25,6 +25,12 @@ export class RedisService {
     return client.get(key);
   }
 
+  async mget(keys: string[]): Promise<(string | null)[]> {
+    if (!keys.length) return [];
+    const client = this.ensureClient();
+    return client.mget(...keys);
+  }
+
   async set(
     key: string,
     value: string,