import { createRedisConnector, resolveRedisConfigFromEnv } from '@sammo-ts/infra'; import { resolveGameApiConfigFromEnv } from '../config.js'; import { buildBattleSimQueueKeys } from './keys.js'; import { processBattleSimJob } from './processor.js'; import { RedisBattleSimTransport } from './redisTransport.js'; import type { BattleSimJob } from './types.js'; type RedisBlPopResult = { key: string; element: string } | [string, string] | null; const parseBlPopValue = (result: RedisBlPopResult): string | null => { if (!result) { return null; } if (Array.isArray(result)) { return result[1] ?? null; } return result.element ?? null; }; export interface BattleSimWorkerOptions { signal?: AbortSignal; } export const runBattleSimWorker = async (options: BattleSimWorkerOptions = {}): Promise => { const config = resolveGameApiConfigFromEnv(); const redis = createRedisConnector(resolveRedisConfigFromEnv()); await redis.connect(); const keys = buildBattleSimQueueKeys(config.profileName); const transport = new RedisBattleSimTransport(redis.client, { keys, requestTimeoutMs: config.battleSimRequestTimeoutMs, resultTtlSeconds: config.battleSimResultTtlSeconds, }); let stopped = options.signal?.aborted ?? false; const handleExit = () => { stopped = true; }; const handleAbort = () => { stopped = true; }; process.on('SIGINT', handleExit); process.on('SIGTERM', handleExit); options.signal?.addEventListener('abort', handleAbort, { once: true }); try { while (!stopped) { // A finite block lets SIGTERM and test AbortSignal stop the worker without // leaving a Redis operation or a detached lifecycle process behind. const item = await redis.client.blPop(keys.queueKey, 1); const raw = parseBlPopValue(item); if (!raw) { continue; } let job: BattleSimJob; try { job = JSON.parse(raw) as BattleSimJob; } catch { continue; } try { const result = processBattleSimJob(job.payload); await transport.pushResult(job.jobId, job.requesterUserId, result); } catch (error) { const reason = error instanceof Error ? error.message : '전투 시뮬레이션 오류'; await transport.pushResult(job.jobId, job.requesterUserId, { result: false, reason, }); } } } finally { process.off('SIGINT', handleExit); process.off('SIGTERM', handleExit); options.signal?.removeEventListener('abort', handleAbort); await redis.disconnect(); } };