All files / sync-engine/domain check-scriptId.service.ts

3.77% Statements 2/53
0% Branches 0/24
0% Functions 0/5
1.92% Lines 1/52

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194                                  1x                                                                                                                                                                                                                                                                                                                                                                
import { AppDataSourceService } from "./data-source.js";
import { SyncEntity } from "../entity/sync.entity.js";
import { AppDataRedisService } from "../domain/data-redis.js";
import { Repository } from "typeorm";
import type { Logger } from "pino";
import { tokens } from "../../registry/tokens.js";
import { autoInjectable, inject, singleton } from "tsyringe";
import { SyncWaitEvent } from "./sync-events.js";
import { Result } from "../../tr8-script/domain/utils/types.js";
 
interface ScriptData {
  scriptId: string;
  scriptType: string;
}
 
@singleton()
@autoInjectable()
class SyncScriptIdService {
  private readonly syncRepository: Repository<SyncEntity>;
  private readonly REDIS_HASH_KEY = "sync-script";
  private readonly REDIS_EXPIRE_TIME = 86400; // 24 hours in seconds
 
  constructor(
    private appDataSourceService: AppDataSourceService,
    private appDataRedisService: AppDataRedisService,
    @inject(tokens.Logger) private readonly logger: Logger,
  ) {
    this.syncRepository =
      this.appDataSourceService.AppDataSource.getRepository(SyncEntity);
  }
 
  private generateKey(scriptId: string, scriptType: string): string {
    return `${scriptId}-${scriptType}`;
  }
 
  private generatePartialKey(
    scriptId: string,
    scriptType: string,
    partial_notif_id: string | null,
  ): string {
    return `${scriptId}-${scriptType}-${partial_notif_id}`;
  }
 
  async syncScriptIdDataJob(): Promise<Result<boolean, Error>> {
    this.logger.info("🔄 Running syncScriptIdData...");
 
    try {
      const syncData = await this.syncRepository
        .createQueryBuilder("sync")
        .select(["sync.scriptId AS scriptId", "sync.scriptType AS scriptType"])
        .getRawMany<ScriptData>();
 
      if (!syncData?.length) {
        this.logger.info("No script ID data found.");
        return { ok: true, value: true };
      }
 
      const pipeline = this.appDataRedisService.redisClient.multi();
 
      for (const { scriptId, scriptType } of syncData) {
        if (!scriptId || !scriptType) {
          this.logger.warn(
            { scriptId, scriptType },
            "Skipping invalid script data entry",
          );
          continue;
        }
 
        const key = this.generateKey(scriptId, scriptType);
        this.logger.info(
          `🔄 Adding scriptId=${scriptId}, scriptType=${scriptType} with key=${key}`,
        );
 
        pipeline.hset(this.REDIS_HASH_KEY, key, scriptType);
      }
 
      await pipeline.exec();
      await this.appDataRedisService.redisClient.expire(
        this.REDIS_HASH_KEY,
        this.REDIS_EXPIRE_TIME,
      );
 
      this.logger.info(
        `✅ Script ID data updated in Redis (${syncData.length} entries).`,
      );
      return { ok: true, value: true };
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);
      this.logger.error(
        { error: errorMessage },
        "Failed to sync script ID data",
      );
      return {
        ok: false,
        error: error instanceof Error ? error : new Error(errorMessage),
      };
    }
  }
 
  async insertScriptIdData(
    event: SyncWaitEvent,
    partial_notif_id?: string | null,
  ): Promise<Result<boolean, Error>> {
    this.logger.info("🔄 Inserting script ID data...");
 
    try {
      if (!event?.scriptId || !event?.scriptType) {
        throw new Error(
          "Invalid event data: scriptId and scriptType are required",
        );
      }
 
      const { scriptId, scriptType } = event;
      const key = partial_notif_id
        ? this.generatePartialKey(scriptId, scriptType, partial_notif_id)
        : this.generateKey(scriptId, scriptType);
      const timestamp = Date.now();
 
      // check if key was already set by gateway
      const existingKey = await this.appDataRedisService.redisClient.get(
        `${this.REDIS_HASH_KEY}:${key}`,
      );
 
      if (existingKey) {
        // Key was already set by gateway, just update hash table for backward compatibility
        await this.appDataRedisService.redisClient.hset(
          this.REDIS_HASH_KEY,
          key,
          scriptType,
        );
 
        this.logger.info(
          `✅ Script ID already reserved by gateway: scriptId=${scriptId}, scriptType=${scriptType}`,
        );
        return { ok: true, value: true };
      }
 
      // Gateway didn't set it, try to set it ourselves (fallback for direct sync events)
      const result = await this.appDataRedisService.redisClient.set(
        `${this.REDIS_HASH_KEY}:${key}`,
        JSON.stringify({
          scriptType,
          timestamp,
          scriptId,
          source: "sync-engine",
        }),
        "EX",
        this.REDIS_EXPIRE_TIME,
        "NX", // Only set if key doesn't exist
      );
 
      if (result === "OK") {
        this.logger.info(
          `🆕 Successfully added scriptId=${scriptId}, scriptType=${scriptType} with key=${key}`,
        );
 
        // Also update the hash for backward compatibility
        await this.appDataRedisService.redisClient.hset(
          this.REDIS_HASH_KEY,
          key,
          scriptType,
        );
 
        return { ok: true, value: true };
      } else {
        // Key already exists, this is a duplicate
        this.logger.info(
          `⚠️ Duplicate detected: scriptId=${scriptId}, scriptType=${scriptType} already exists`,
        );
        return {
          ok: false,
          error: new Error(
            `Duplicate scriptId=${scriptId}, scriptType=${scriptType}`,
          ),
        };
      }
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);
      this.logger.error(
        { error: errorMessage },
        "Error inserting script ID data",
      );
      return {
        ok: false,
        error: error instanceof Error ? error : new Error(errorMessage),
      };
    }
  }
}
 
export { SyncScriptIdService };