فهرست منبع

refactor: replace manual epoch time calculations with nowSecBigInt utility across services

Dave 1 ماه پیش
والد
کامیت
5b554591a9

+ 2 - 1
.gitignore

@@ -2,7 +2,7 @@
 /dist
 /node_modules
 action-plans/
-docs/
+docs/*
 
 # Logs
 logs
@@ -71,3 +71,4 @@ src/tmp/*
 docker/mongo/mongo-keyfile
 action-plans/20251222-ACT-01.md
 .codex-instructions.md
+timestamp-audit.json

+ 4 - 4
apps/box-app-api/src/feature/ads/ad.service.ts

@@ -25,7 +25,7 @@ import {
 } from '../../rabbitmq/rabbitmq-publisher.service';
 import { AdsClickEventPayload } from '@box/common/events/ads-click-event.dto';
 import { randomUUID } from 'crypto';
-import { nowEpochMsBigInt } from '@box/common/time/time.util';
+import { nowEpochMsBigInt, nowSecBigInt } from '@box/common/time/time.util';
 
 // This should match what mgnt-side rebuildSingleAdCache stores.
 // We only care about a subset for now.
@@ -278,7 +278,7 @@ export class AdService {
 
         // Query MongoDB for full ad details
         try {
-          const now = BigInt(Math.floor(Date.now() / 1000));
+          const now = nowSecBigInt();
           const ads = await this.mongoPrisma.ads.findMany({
             where: {
               id: { in: adIds },
@@ -403,7 +403,7 @@ export class AdService {
     // Step 4: Query MongoDB for full ad details
     let ads: Awaited<ReturnType<typeof this.mongoPrisma.ads.findMany>>;
     try {
-      const now = BigInt(Math.floor(Date.now() / 1000));
+      const now = nowSecBigInt();
       ads = await this.mongoPrisma.ads.findMany({
         where: {
           id: { in: adIds },
@@ -496,7 +496,7 @@ export class AdService {
         `Ad cache miss: adsId=${adsId}, key=${adKey}, falling back to MongoDB`,
       );
 
-      const now = BigInt(Math.floor(Date.now() / 1000));
+      const now = nowSecBigInt();
       const ad = await this.mongoPrisma.ads.findUnique({
         where: { id: adsId },
         select: {

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

@@ -1,6 +1,7 @@
 // apps/box-app-api/src/feature/auth/auth.service.ts
 import { Injectable, Logger } from '@nestjs/common';
 import { UserLoginEventPayload } from '@box/common/events/user-login-event.dto';
+import { nowSecBigInt } from '@box/common/time/time.util';
 import { RabbitmqPublisherService } from '../../rabbitmq/rabbitmq-publisher.service';
 import { PrismaMongoStatsService } from '../../prisma/prisma-mongo-stats.service';
 import { PrismaMongoService } from '../../prisma/prisma-mongo.service';
@@ -36,7 +37,7 @@ export class AuthService {
     const channelIdInput = this.normalizeOptional(params.channelId);
     const machine = this.normalizeOptional(params.machine);
 
-    const nowSec = BigInt(Math.floor(Date.now() / 1000)); // BigInt epoch seconds
+    const nowSec = nowSecBigInt(); // BigInt epoch seconds
 
     // 1) Find user (in box-admin)
     let user = await this.getUserByUid(uid);

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

@@ -10,6 +10,7 @@ import {
   EnrichedVideoRecommendationDto,
   EnrichedAdRecommendationDto,
 } from './dto/enriched-recommendation.dto';
+import { nowSecBigInt } from '@box/common/time/time.util';
 
 interface VideoCandidate {
   videoId: string;
@@ -311,7 +312,7 @@ export class RecommendationService {
 
     try {
       // 1. Get current timestamp for date filtering
-      const now = BigInt(Math.floor(Date.now() / 1000));
+      const now = nowSecBigInt();
 
       // 2. Fetch eligible ads from Mongo with strict filters
       const eligibleAds = await this.prisma.ads.findMany({

+ 3 - 6
apps/box-mgnt-api/src/mgnt-backend/feature/ads/ads.service.ts

@@ -21,6 +21,7 @@ import {
 import { CommonStatus } from '../common/status.enum';
 import { ImageUrlBuilderService } from './image/image-url-builder.service';
 import type { AdType as PrismaAdType } from '@prisma/mongo/client';
+import { nowSecBigInt, toSecBigInt } from '@box/common/time/time.util';
 
 /**
  * MIGRATION NOTES:
@@ -40,16 +41,12 @@ export class AdsService {
   ) {}
 
   private nowSeconds(): bigint {
-    return BigInt(Math.floor(Date.now() / 1000));
+    return nowSecBigInt();
   }
 
   private toBigIntSeconds(value?: number | bigint | null): bigint | undefined {
     if (value === undefined || value === null) return undefined;
-    if (typeof value === 'bigint') return value;
-    if (typeof value === 'number' && Number.isFinite(value)) {
-      return BigInt(Math.floor(value));
-    }
-    return BigInt(value);
+    return toSecBigInt(value);
   }
 
   private trimOptional(value?: string | null) {

+ 2 - 1
apps/box-mgnt-api/src/mgnt-backend/feature/ads/image/ads-image.service.ts

@@ -4,6 +4,7 @@ import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
 import { LocalImageStorageService } from './storage/local-image-storage.service';
 import { S3ImageStorageService } from './storage/s3-image-storage.service';
 import { ImageUrlBuilderService } from '../image/image-url-builder.service';
+import { nowSecBigInt } from '@box/common/time/time.util';
 
 interface UploadAdCoverResult {
   id: string;
@@ -39,7 +40,7 @@ export class AdsImageService {
     const newKey = this.local.getRelativeKeyFromAbsolutePath(savedAbsPath);
 
     // 2. Update Ads record to LOCAL_ONLY
-    const now = BigInt(Math.floor(Date.now() / 1000));
+    const now = nowSecBigInt();
     const updated = await this.mongo.ads.update({
       where: { id: adId },
       data: {

+ 5 - 5
apps/box-mgnt-api/src/mgnt-backend/feature/category/category.service.ts

@@ -15,6 +15,7 @@ import {
   ListCategoryDto,
   UpdateCategoryDto,
 } from './category.dto';
+import { nowSecBigInt } from '@box/common/time/time.util';
 import { CommonStatus } from '../common/status.enum';
 
 @Injectable()
@@ -25,14 +26,13 @@ export class CategoryService {
   ) {}
 
   /**
-   * Current epoch time in milliseconds.
+   * Current epoch seconds (BigInt) for persisted timestamps.
    *
    * NOTE:
-   *  - Kept as `number` for backward compatibility.
-   *  - If you migrate to BigInt timestamps, change this and the Prisma schema accordingly.
+   *  - We now save BigInt seconds in Prisma.
    */
-  private now(): number {
-    return Date.now();
+  private now(): bigint {
+    return nowSecBigInt();
   }
 
   /**

+ 5 - 5
apps/box-mgnt-api/src/mgnt-backend/feature/channel/channel.service.ts

@@ -16,6 +16,7 @@ import {
   ListChannelDto,
   UpdateChannelDto,
 } from './channel.dto';
+import { nowSecBigInt } from '@box/common/time/time.util';
 
 @Injectable()
 export class ChannelService {
@@ -25,14 +26,13 @@ export class ChannelService {
   ) {}
 
   /**
-   * Current epoch time in milliseconds.
+   * Current epoch seconds (BigInt) for persisted timestamps.
    *
    * NOTE:
-   *  - Kept as `number` for backward compatibility.
-   *  - If you migrate to BigInt timestamps, update this and the Prisma schema.
+   *  - We now keep `createAt`/`updateAt` as BigInt seconds.
    */
-  private now(): number {
-    return Date.now();
+  private now(): bigint {
+    return nowSecBigInt();
   }
 
   private trimOptional(value?: string | null) {

+ 3 - 2
apps/box-mgnt-api/src/mgnt-backend/feature/system-params/system-params.service.ts

@@ -8,14 +8,15 @@ import {
   UpdateSystemParamDto,
 } from './system-param.dto';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
+import { nowSecBigInt } from '@box/common/time/time.util';
 
 @Injectable()
 export class SystemParamsService {
   constructor(private readonly mongoPrismaService: MongoPrismaService) {}
 
   // ---------- Helpers ----------
-  private nowSec() {
-    return Math.floor(Date.now() / 1000);
+  private nowSec(): bigint {
+    return nowSecBigInt();
   }
 
   private normalizeValue(value: string, type: ParamDataType): string {

+ 5 - 6
apps/box-mgnt-api/src/mgnt-backend/feature/tag/tag.service.ts

@@ -13,6 +13,7 @@ import {
 import { CreateTagDto, ListTagDto, UpdateTagDto } from './tag.dto';
 import { CommonStatus } from '../common/status.enum';
 import { CategoryService } from '../category/category.service';
+import { nowSecBigInt } from '@box/common/time/time.util';
 
 @Injectable()
 export class TagService {
@@ -23,15 +24,13 @@ export class TagService {
   ) {}
 
   /**
-   * Current epoch time in milliseconds.
+   * Current epoch seconds (BigInt) for persisted timestamps.
    *
    * NOTE:
-   *  - For now we keep this as `number` for backward compatibility.
-   *  - If you migrate to BigInt timestamps later, change this to return `bigint`
-   *    and update the Prisma schema + DTOs accordingly.
+   *  - `createAt`/`updateAt` now persist as BigInt seconds.
    */
-  private now(): number {
-    return Date.now();
+  private now(): bigint {
+    return nowSecBigInt();
   }
 
   private async scheduleCategoryCaches(categoryId: string): Promise<void> {

+ 25 - 0
libs/common/src/time/time.util.spec.ts

@@ -0,0 +1,25 @@
+import { nowSecBigInt, toSecBigInt } from './time.util';
+
+describe('time util helpers', () => {
+  it('treats large numbers as milliseconds when converting to seconds', () => {
+    const msValue = 1_600_000_123_456;
+    expect(toSecBigInt(msValue)).toBe(BigInt(Math.floor(msValue / 1000)));
+  });
+
+  it('leaves second-precision numbers intact', () => {
+    const secValue = 1_640_000_000;
+    expect(toSecBigInt(secValue)).toBe(BigInt(secValue));
+  });
+
+  it('passes through bigint inputs unchanged', () => {
+    const big = BigInt(1_640_000_000);
+    expect(toSecBigInt(big)).toBe(big);
+  });
+
+  it('returns a bigint close to Date.now()/1000', () => {
+    const nowSec = nowSecBigInt();
+    const approx = BigInt(Math.floor(Date.now() / 1000));
+    expect(nowSec >= approx - BigInt(1)).toBe(true);
+    expect(nowSec <= approx + BigInt(1)).toBe(true);
+  });
+});

+ 31 - 0
libs/common/src/time/time.util.ts

@@ -1,4 +1,35 @@
 // Utility helpers for time-related operations
+const MS_THRESHOLD = 1_000_000_000_000;
+
+/**
+ * Strictly for persisted timestamps:
+ * return epoch seconds as bigint (floor)
+ */
+export function nowSecBigInt(): bigint {
+  return BigInt(Math.floor(Date.now() / 1000));
+}
+
+/**
+ * Convert a runtime value into epoch seconds (bigint).
+ * Numbers > 1e12 are treated as milliseconds and converted down.
+ */
+export function toSecBigInt(value: number | bigint): bigint {
+  if (typeof value === 'bigint') {
+    return value;
+  }
+  if (!Number.isFinite(value)) {
+    return BigInt(0);
+  }
+  if (Math.abs(value) > MS_THRESHOLD) {
+    return BigInt(Math.floor(value / 1000));
+  }
+  return BigInt(Math.floor(value));
+}
+
 export function nowEpochMsBigInt(): bigint {
   return BigInt(Date.now());
 }
+
+export function nowMs(): number {
+  return Date.now();
+}

+ 3 - 2
libs/core/src/ad/ad-cache-warmup.service.ts

@@ -4,6 +4,7 @@ import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import { RedisService } from '@box/db/redis/redis.service';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
 import type { AdType } from '@box/common/ads/ad-types';
+import { nowSecBigInt } from '@box/common/time/time.util';
 
 interface CachedAd {
   id: string;
@@ -55,7 +56,7 @@ export class AdCacheWarmupService implements OnModuleInit {
    */
   async warmupAllAdCaches(): Promise<void> {
     const startTime = Date.now();
-    const now = BigInt(Math.floor(Date.now() / 1000));
+    const now = nowSecBigInt();
 
     try {
       // Fetch all active ads
@@ -136,7 +137,7 @@ export class AdCacheWarmupService implements OnModuleInit {
    * Warm up a single ad cache (used for on-demand refresh)
    */
   async warmupSingleAd(adId: string): Promise<void> {
-    const now = BigInt(Math.floor(Date.now() / 1000));
+    const now = nowSecBigInt();
 
     const ad = await this.mongoPrisma.ads.findUnique({
       where: { id: adId },

+ 3 - 2
libs/core/src/ad/ad-pool.service.ts

@@ -6,6 +6,7 @@ import type { AdPoolEntry, AdType } from '@box/common/ads/ad-types';
 import { AdType as PrismaAdType } from '@prisma/mongo/client';
 import { RedisService } from '@box/db/redis/redis.service';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
+import { nowSecBigInt } from '@box/common/time/time.util';
 
 export type AdPayload = AdPoolEntry & {
   trackingId?: string;
@@ -51,7 +52,7 @@ export class AdPoolService {
 
   /** Rebuild a single ad pool for an AdType. Returns number of ads written. */
   async rebuildPoolForType(adType: AdType): Promise<number> {
-    const now = BigInt(Math.floor(Date.now() / 1000));
+    const now = nowSecBigInt();
 
     const ads = await this.mongoPrisma.ads.findMany({
       where: {
@@ -133,7 +134,7 @@ export class AdPoolService {
   /** Fallback: pick a random ad directly from MongoDB. */
   async getRandomFromDb(adType: AdType): Promise<AdPayload | null> {
     try {
-      const now = BigInt(Math.floor(Date.now() / 1000));
+      const now = nowSecBigInt();
       const ads = await this.mongoPrisma.ads.findMany({
         where: {
           adType,

+ 2 - 1
prisma/mongo/seed-ads.ts

@@ -4,6 +4,7 @@ import {
   ImageSource,
   Prisma,
 } from '@prisma/mongo/client';
+import { nowSecBigInt } from '@box/common/time/time.util';
 
 const prisma = new PrismaClient();
 
@@ -34,7 +35,7 @@ function parseArgs(argv: string[]): SeedArgs {
 }
 
 function nowEpochSec(): bigint {
-  return BigInt(Math.floor(Date.now() / 1000));
+  return nowSecBigInt();
 }
 
 function randInt(min: number, max: number): number {