@hostwebhook/node-sdk 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,14 +5,7 @@
5
5
  * _meta.iterable and automatically iterates, aggregating results.
6
6
  * Nodes never see _meta — they receive clean individual payloads.
7
7
  */
8
- type SingleExecutor = (payload: Record<string, unknown>) => Promise<{
9
- statusCode: number;
10
- responseBody: string;
11
- latencyMs: number;
12
- }>;
13
- export declare function executeWithIteration(payload: Record<string, unknown>, executeSingle: SingleExecutor): Promise<{
14
- statusCode: number;
15
- responseBody: string;
16
- latencyMs: number;
17
- }>;
8
+ import type { NodeResult } from './retry-transient';
9
+ type SingleExecutor = (payload: Record<string, unknown>) => Promise<NodeResult>;
10
+ export declare function executeWithIteration(payload: Record<string, unknown>, executeSingle: SingleExecutor): Promise<NodeResult>;
18
11
  export {};
@@ -14,7 +14,46 @@ async function executeWithIteration(payload, executeSingle) {
14
14
  const items = payload[meta.iterateField] ?? [];
15
15
  const results = [];
16
16
  let totalLatency = 0;
17
- let lastStatus = 200;
17
+ /* El estado que se reporta hacia arriba.
18
+ *
19
+ * ⚠️ Antes se guardaba el del ÚLTIMO item, a secas. Cuatro llamadas donde
20
+ * la 2ª daba 500 y la 4ª daba 200 se reportaban como ÉXITO —el fallo
21
+ * desaparecía sin dejar rastro— y al revés, tres éxitos y un fallo al final
22
+ * teñían de rojo la tanda entera.
23
+ *
24
+ * Ahora manda el PRIMER fallo; si no hubo ninguno se conserva el último
25
+ * estado, que es exactamente lo que ya se devolvía cuando todo iba bien.
26
+ *
27
+ * ## Lo que esto NO pisa, y lo que sí
28
+ *
29
+ * NO pisa el «Never error on non-2xx» del nodo HTTP, y el motivo es el
30
+ * orden de anidamiento, no una casualidad: la máscara vive en
31
+ * `executeOnce`, la función MÁS interna —dentro del retry, que a su vez
32
+ * está dentro de este bucle—, y axios va con `validateStatus: () => true`,
33
+ * así que TODO código de estado pasa por ella antes de que nadie más lo
34
+ * vea. Aquí llega un 200 y `primerFallo` no se marca.
35
+ *
36
+ * ⚠️ Lo que sí cambia, y conviene saberlo: el ajuste sólo enmascara
37
+ * `response.status >= 400`. Un timeout, un DNS caído o un fallo de
38
+ * configuración salen por otro camino (500 del `catch`, 520 del guion de
39
+ * pre-petición o del firmado) sin pasar por la máscara. Antes esos se
40
+ * tragaban si no eran el último item; ahora detienen la tanda aunque el
41
+ * ajuste esté encendido. Es el comportamiento correcto —un timeout no es
42
+ * «un non-2xx que quiero ignorar»— pero es un cambio observable. */
43
+ let ultimoStatus = 200;
44
+ let primerFallo = null;
45
+ /* Plano de control. No viaja en el payload, pero decide qué hace el Loop
46
+ * cuando una iteración falla.
47
+ *
48
+ * ⚠️ La agregación construía un objeto literal nuevo y estos dos campos se
49
+ * caían por el camino. Daba casi igual mientras el fallo de enmedio se
50
+ * perdía, porque esa rama no se alcanzaba; en cuanto manda el primer
51
+ * fallo, `node-lifecycle` lee `_retriedTransient` para degradar la política
52
+ * `retry` a `skip`, y sin él RE-DESPACHA la iteración entera: se reenvían
53
+ * también los items que ya habían salido bien. Un 429 que ya quemó sus
54
+ * tres intentos le llegaba al Loop como «esto ni se ha reintentado». */
55
+ let algunoReintento = false;
56
+ let esperaPedida;
18
57
  for (const item of items) {
19
58
  const clean = typeof item === 'object' && item !== null
20
59
  ? { ...item }
@@ -22,7 +61,17 @@ async function executeWithIteration(payload, executeSingle) {
22
61
  delete clean._meta;
23
62
  const res = await executeSingle(clean);
24
63
  totalLatency += res.latencyMs;
25
- lastStatus = res.statusCode;
64
+ ultimoStatus = res.statusCode;
65
+ if (primerFallo === null && res.statusCode >= 400) {
66
+ primerFallo = res.statusCode;
67
+ }
68
+ if (res._retriedTransient)
69
+ algunoReintento = true;
70
+ if (typeof res.retryAfterMs === 'number') {
71
+ /* El mayor de los que se pidieron: esperar MENOS de lo que el servidor
72
+ dijo sólo quema un intento. */
73
+ esperaPedida = Math.max(esperaPedida ?? 0, res.retryAfterMs);
74
+ }
26
75
  try {
27
76
  const parsed = JSON.parse(res.responseBody);
28
77
  // If executor returns an array, spread its items instead of nesting [[...]]
@@ -38,15 +87,23 @@ async function executeWithIteration(payload, executeSingle) {
38
87
  results.push(res.responseBody);
39
88
  }
40
89
  }
90
+ const estadoDeLaTanda = primerFallo ?? ultimoStatus;
91
+ /* Se omiten cuando no hay nada que decir, para no ensuciar el resultado con
92
+ `_retriedTransient: false` donde antes no había clave ninguna. */
93
+ const planoDeControl = {
94
+ ...(algunoReintento ? { _retriedTransient: true } : {}),
95
+ ...(esperaPedida !== undefined ? { retryAfterMs: esperaPedida } : {}),
96
+ };
41
97
  // Single result from single iteration → unwrap as plain object
42
98
  if (results.length === 1 &&
43
99
  typeof results[0] === 'object' &&
44
100
  results[0] !== null) {
45
101
  const single = { _meta: { iterable: false, count: 1 }, ...results[0] };
46
102
  return {
47
- statusCode: lastStatus,
103
+ statusCode: estadoDeLaTanda,
48
104
  responseBody: JSON.stringify(single),
49
105
  latencyMs: totalLatency,
106
+ ...planoDeControl,
50
107
  };
51
108
  }
52
109
  const aggregated = {
@@ -54,9 +111,10 @@ async function executeWithIteration(payload, executeSingle) {
54
111
  results,
55
112
  };
56
113
  return {
57
- statusCode: lastStatus,
114
+ statusCode: estadoDeLaTanda,
58
115
  responseBody: JSON.stringify(aggregated),
59
116
  latencyMs: totalLatency,
117
+ ...planoDeControl,
60
118
  };
61
119
  }
62
120
  // Single item — strip _meta and execute
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/node-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "El SDK de un nodo de HostWebhook: ciclo de vida, ejecución con iteración, reintentos, filtros y validación de esquema — lo que comparten la api y el futuro servicio de nodos",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",