Files
sap-legacy-webhook-emulator/src/dispatcher/evn1-repository.ts
T
george.calle 5a2e149825 Dispatcher: evitar ticks solapados, enviar en lotes y mejorar logs
- 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.
2026-10-20 15:26:39 -03:00

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}')`);
}