- index.ts: guard tickRunning; un tick largo ya no se solapa con el siguiente de node-cron (causaba -2035 en @EVN1 y -1116 cruzados por requests concurrentes sobre la misma sesion de Service Layer). - process-tick.ts: un evento con error ya no corta el tick; progreso cada 100 eventos; envios en lotes de MAX_EVENTS_PER_BATCH (default 30) para no superar el tamano de body del receptor (HTTP 413). - evn1-repository.ts: findExisting falla si el GET falla, en vez de tratarlo como "no existe" y crear un duplicado. - service-layer-client.ts: errores incluyen metodo, ruta y body. - logger.ts: no poder escribir el archivo de log no tumba el proceso. - CLAUDE.md: notas operativas y mejoras pendientes.
98 lines
3.8 KiB
TypeScript
98 lines
3.8 KiB
TypeScript
import type { ServiceLayerClient } from '../service-layer-client.ts';
|
|
import { generateCode } from '../lib/codes.ts';
|
|
import { hhmmss } from '../lib/timestamps.ts';
|
|
import { userTableEntity } from '../lib/service-layer-paths.ts';
|
|
|
|
const ENTITY = userTableEntity('EVN1');
|
|
|
|
export interface Evn1Row {
|
|
code: string;
|
|
eventId: string;
|
|
webhookId: string;
|
|
status: string;
|
|
retry: number;
|
|
}
|
|
|
|
function toRow(raw: Record<string, unknown>): Evn1Row {
|
|
return {
|
|
code: raw.Code as string,
|
|
eventId: raw.U_EventID as string,
|
|
webhookId: raw.U_WebhookID as string,
|
|
status: raw.U_Status as string,
|
|
retry: (raw.U_Retry as number) ?? 0,
|
|
};
|
|
}
|
|
|
|
export async function findExisting(client: ServiceLayerClient, eventId: string, webhookId: string): Promise<Evn1Row | null> {
|
|
const filter = encodeURIComponent(`U_EventID eq '${eventId}' and U_WebhookID eq '${webhookId}'`);
|
|
const res = await client.get(`/${ENTITY}?$filter=${filter}`);
|
|
// Un GET fallido no es "no existe": tratarlo así lleva a un POST duplicado.
|
|
if (res.status !== 200) {
|
|
throw new Error(`Service Layer respondió HTTP ${res.status} buscando @EVN1 de ${eventId}: ${JSON.stringify(res.data)}`);
|
|
}
|
|
const rows = (res.data?.value as Record<string, unknown>[]) ?? [];
|
|
return rows.length > 0 ? toRow(rows[0]) : null;
|
|
}
|
|
|
|
export async function create(client: ServiceLayerClient, eventId: string, webhookId: string): Promise<void> {
|
|
await client.post(`/${ENTITY}`, {
|
|
Code: generateCode(),
|
|
Name: `${eventId}/${webhookId}`,
|
|
U_EventID: eventId,
|
|
U_WebhookID: webhookId,
|
|
U_Status: 'F',
|
|
U_Retry: 0,
|
|
});
|
|
}
|
|
|
|
export async function listPending(client: ServiceLayerClient): Promise<Evn1Row[]> {
|
|
const rows = await client.getAll(`/${ENTITY}?$filter=${encodeURIComponent("U_Status eq 'F'")}`);
|
|
return rows.map(toRow);
|
|
}
|
|
|
|
export async function listForEvent(client: ServiceLayerClient, eventId: string): Promise<Evn1Row[]> {
|
|
const rows = await client.getAll(`/${ENTITY}?$filter=${encodeURIComponent(`U_EventID eq '${eventId}'`)}`);
|
|
return rows.map(toRow);
|
|
}
|
|
|
|
export async function markDelivered(client: ServiceLayerClient, code: string, returnCode: number): Promise<void> {
|
|
const now = new Date();
|
|
await client.patch(`/${ENTITY}('${code}')`, {
|
|
U_Status: 'D',
|
|
U_ReturnCode: returnCode,
|
|
U_LastSentDT: now.toISOString(),
|
|
U_LastSentTM: hhmmss(now),
|
|
});
|
|
}
|
|
|
|
export async function markFailedAttempt(client: ServiceLayerClient, row: Evn1Row, returnCode: number): Promise<void> {
|
|
const now = new Date();
|
|
await client.patch(`/${ENTITY}('${row.code}')`, {
|
|
U_Retry: row.retry + 1,
|
|
U_ReturnCode: returnCode,
|
|
U_LastSentDT: now.toISOString(),
|
|
U_LastSentTM: hhmmss(now),
|
|
});
|
|
}
|
|
|
|
// Pone en 0 el contador de reintentos de las entregas pendientes de una
|
|
// suscripción. Se usa al reactivarla (State X -> A): sin esto, las filas
|
|
// siguen con Retry >= RETRY_LIMIT y la suscripción vuelve a X en el
|
|
// siguiente tick sin intentar enviar nada.
|
|
export async function resetRetries(client: ServiceLayerClient, webhookId: string): Promise<number> {
|
|
const filter = encodeURIComponent(`U_WebhookID eq '${webhookId}' and U_Status eq 'F'`);
|
|
const rows = (await client.getAll(`/${ENTITY}?$filter=${filter}`)).map(toRow).filter((r) => r.retry > 0);
|
|
for (const row of rows) await client.patch(`/${ENTITY}('${row.code}')`, { U_Retry: 0 });
|
|
return rows.length;
|
|
}
|
|
|
|
// Entregas fallidas/pendientes (Status='F') de una suscripción.
|
|
export async function listPendingForWebhook(client: ServiceLayerClient, webhookId: string): Promise<Evn1Row[]> {
|
|
const filter = encodeURIComponent(`U_WebhookID eq '${webhookId}' and U_Status eq 'F'`);
|
|
return (await client.getAll(`/${ENTITY}?$filter=${filter}`)).map(toRow);
|
|
}
|
|
|
|
export async function deleteRow(client: ServiceLayerClient, code: string): Promise<void> {
|
|
await client.delete(`/${ENTITY}('${code}')`);
|
|
}
|