Files
MikrotikManager/backend/src/services/alert-engine/outbox-execution.ts
T

36 lines
3.6 KiB
TypeScript

import {z} from "zod"
import type {TelegramSendResult} from "../alerts-telegram-send.js"
const payloadSchema=z.object({text:z.string().min(1),chatId:z.string().optional(),history:z.object({id:z.string().min(1),ruleId:z.string().nullable().optional(),groupId:z.string().nullable().optional(),ruleName:z.string(),severity:z.enum(["critical","warning","info"]),message:z.string(),firedAt:z.string().refine(value=>Number.isFinite(Date.parse(value)))}).strict()}).strict()
export type AlertOutboxPayload=z.output<typeof payloadSchema>
export type OutboxRow={id:string;retryCount:number;maxRetries:number;payloadJson:unknown}
export type OutboxSettlement={status:"pending"|"sent"|"failed"|"unknown";retryCount?:number;nextAttemptAt?:string;sentAt?:string;lastError:string|null;history?:AlertOutboxPayload["history"]&{sentOk:boolean}}
type Result={kind:"sent"|"failed"|"retry"|"held"|"skipped";apiAccepted?:true;error?:string}
interface Dependencies{claim:(id:string)=>Promise<OutboxRow|undefined>;send:(payload:AlertOutboxPayload)=>Promise<TelegramSendResult>;settle:(id:string,value:OutboxSettlement)=>Promise<void>;now:()=>number}
export async function executeOutboxDelivery(id:string,deps:Dependencies):Promise<Result>{
let row:OutboxRow|undefined
try{row=await deps.claim(id)}catch{return {kind:"held",error:"Не подтверждён захват сообщения; отправка не начата"}}
if(!row)return {kind:"skipped"}
if(row.id!==id)return {kind:"held",error:"Захвачено другое сообщение; отправка не начата"}
async function hold(error:string,apiAccepted?:true):Promise<Result>{
try{await deps.settle(id,{status:"unknown",lastError:error})}catch{return {kind:"held",apiAccepted,error:error+". Сохранение статуса не подтверждено; processing не повторяется автоматически"}}
return {kind:"held",apiAccepted,error}
}
let raw=row.payloadJson
if(typeof raw==="string"){try{raw=JSON.parse(raw)}catch{raw=null}}
const parsed=payloadSchema.safeParse(raw)
if(!parsed.success||!Number.isInteger(row.retryCount)||row.retryCount<0||!Number.isInteger(row.maxRetries)||row.maxRetries<1){
try{await deps.settle(id,{status:"failed",lastError:"Некорректная запись очереди"});return {kind:"failed"}}catch{return hold("Не удалось отклонить некорректную запись очереди")}
}
const payload=parsed.data
let send:TelegramSendResult
try{send=await deps.send(payload)}catch{return hold("Результат отправки неизвестен: ответ не получен")}
if(send?.ok===true){
try{await deps.settle(id,{status:"sent",sentAt:new Date(deps.now()).toISOString(),lastError:null,history:{...payload.history,sentOk:true}});return {kind:"sent",apiAccepted:true}}
catch{return hold("Telegram API принял сообщение, но запись результата в БД не подтверждена",true)}
}
if(send?.ok!==false||send.outcome!=="rejected")return hold(send?.ok===false?send.error:"Telegram не подтвердил результат отправки")
const retryCount=row.retryCount+1,exhausted=retryCount>=row.maxRetries
try{await deps.settle(id,{retryCount,status:exhausted?"failed":"pending",nextAttemptAt:new Date(deps.now()+Math.min(300_000,retryCount*15_000)).toISOString(),lastError:send.error,...(exhausted?{history:{...payload.history,sentOk:false}}:{})});return {kind:exhausted?"failed":"retry",error:send.error}}
catch{return hold("Отказ Telegram получен, но запись следующей попытки не подтверждена")}
}