Apuntes DAM
Volver al inicio

Cargador de datos asíncrono con reintentos y dependencias

Ejercicio de JavaScriptMuy difícilUnos 90 minutos

Carga en paralelo los datos de un panel desde varias «APIs» simuladas: tiempo límite con Promise.race, reintentos con async/await, recursos que dependen de otros y un informe final en orden. Promesas, async/await, Promise.all, setTimeout y gestión de errores asíncronos.

  • Promise y setTimeout
  • async / await
  • Promise.all y Promise.race
  • try/catch con await
  • Memorizar promesas en un Map
  • Funciones async inmediatas

Enunciado

El panel de control de una tienda online pide al arrancar los usuarios, los productos, los pedidos, las estadísticas… cada dato a una API distinta. Pedirlos uno detrás de otro haría esperar al usuario la suma de todos los tiempos; pedirlos a la vez, solo lo que tarde el más lento.

Pero las redes fallan. Una petición puede devolver un error (y quizá funcione si se repite) o quedarse colgada para siempre: por eso cada petición tiene un tiempo límite de 100 ms y se reintenta hasta 3 veces. Además, algunos datos dependen de otros (los pedidos necesitan los usuarios): esos tienen que esperar a sus dependencias, y si alguna falla, no se piden.

Como no hay APIs reales, se simulan con setTimeout: cada recurso tarda unos milisegundos y falla las primeras veces que se indique. El informe final va en el orden de la entrada, no en el de llegada, para que sea siempre el mismo.

Qué tiene que hacer el programa

  1. Cada línea es nombre ms fallos [dependencias]: lo que tarda cada petición en milisegundos, cuántas de las primeras peticiones fallan con error del servidor y, opcionalmente, los nombres de los recursos de los que depende separados por comas (sin espacios). Las líneas vacías se ignoran. Una línea con otro formato o con un nombre repetido escribe Aviso: línea N ignorada. No hay dependencias circulares.
  2. Todos los recursos empiezan a cargarse a la vez, salvo los que tienen dependencias, que esperan a que terminen todas ellas. Si alguna dependencia no se ha cargado (o no existe), el recurso no se pide.
  3. Cada intento dura lo que diga ms, pero si pasan 100 ms sin respuesta se da por fallido con tiempo agotado. Se hacen hasta 3 intentos, uno detrás de otro, y el motivo de error que se informa es el del último.
  4. Al terminar todo, una línea por recurso en el orden de la entrada: nombre: cargado (N intentos) («intento» si es 1), nombre: error tras 3 intentos (motivo) u nombre: omitido (depende de dep, que no se ha podido cargar) / (depende de dep, que no existe), indicando la primera dependencia de su lista que ha fallado.
  5. Para terminar, Cargados: C de T · Peticiones: P, donde P es el número total de peticiones simuladas que se han hecho.

Entrada

Un recurso por línea: nombre ms fallos y, opcionalmente, dep1,dep2.

Ejemplos de ejecución

Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.

Un panel con seis recursos

Entrada

usuarios 30 0
productos 20 2
fotos 150 0
pedidos 10 0 usuarios,productos
galeria 10 0 fotos
informe 10 0 pedidos,stock

Salida por consola

usuarios: cargado (1 intento)
productos: cargado (3 intentos)
fotos: error tras 3 intentos (tiempo agotado)
pedidos: cargado (1 intento)
galeria: omitido (depende de fotos, que no se ha podido cargar)
informe: omitido (depende de stock, que no existe)
Cargados: 3 de 6 · Peticiones: 8

Errores y líneas no válidas

Entrada

a 10 3
b 10 1
b 20 0
c diez 0
d 5 0 a

Salida por consola

Aviso: línea 3 ignorada
Aviso: línea 4 ignorada
a: error tras 3 intentos (error del servidor)
b: cargado (2 intentos)
d: omitido (depende de a, que no se ha podido cargar)
Cargados: 1 de 3 · Peticiones: 5

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. Una promesa que se resuelve sola

La petición simulada ya la tienes: es una Promise que llama a resolve o a reject dentro de un setTimeout. Observa que cuenta la petición en el momento de crearla.

2. Tiempo límite con Promise.race

Promise.race termina con la primera de sus promesas que termine. Si haces correr la petición contra un temporizador que rechaza a los 100 ms, obtienes una petición con tiempo límite.

javascript
const limite = new Promise((_, reject) => setTimeout(() => reject(new Error("tiempo agotado")), ms));
return Promise.race([promesa, limite]);
3. Reintentos con await

Con async/await un bucle de reintentos se escribe como si fuera síncrono: try { await ...; return éxito } catch (e) { guarda el motivo } dentro de un for. Si el await lanza, se pasa al siguiente intento.

4. Cargar cada recurso una sola vez

Si dos recursos dependen de usuarios, no hay que pedirlo dos veces. Guarda en un Map la promesa de cada carga: la primera llamada la crea y las siguientes devuelven la misma. Dentro, espera las dependencias con await Promise.all(...) antes de pedir el recurso.

javascript
if (!cargas.has(nombre)) cargas.set(nombre, (async () => { ... })());
return cargas.get(nombre);
5. El informe en orden

Promise.all devuelve los resultados en el orden de las promesas que recibió, no en el orden en que terminan. Pasándole las cargas en el orden de la entrada, el informe sale siempre igual.

Resuélvelo aquí

El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 2 casos ocultos que buscan los errores típicos.

🟨JavaScriptCargador de datos asíncrono con reintentos y dependenciasMuy difícil

Ejemplo

Entrada (lo que se escribe por teclado)
usuarios 30 0
productos 20 2
fotos 150 0
pedidos 10 0 usuarios,productos
galeria 10 0 fotos
informe 10 0 pedidos,stock
Salida esperada
usuarios: cargado (1 intento)
productos: cargado (3 intentos)
fotos: error tras 3 intentos (tiempo agotado)
pedidos: cargado (1 intento)
galeria: omitido (depende de fotos, que no se ha podido cargar)
informe: omitido (depende de stock, que no existe)
Cargados: 3 de 6 · Peticiones: 8
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada

Solución explicada

Ver la solución completa
javascript
1const lineas = require("fs").readFileSync(0, "utf8").split("\n");
2
3const LIMITE_MS = 100;
4const INTENTOS = 3;
5let peticiones = 0;
6
7/** Petición simulada: tarda «ms» y falla mientras el intento sea menor o igual que «fallos». */
8function peticion(recurso, intento) {
9  peticiones++;
10  return new Promise((resolve, reject) => {
11    setTimeout(() => (intento <= recurso.fallos ? reject(new Error("error del servidor")) : resolve(recurso.nombre)), recurso.ms);
12  });
13}
14
15/** La primera promesa que termine gana: la petición o el temporizador. */
16function conTiempoLimite(promesa, ms) {
17  const limite = new Promise((_, reject) => setTimeout(() => reject(new Error("tiempo agotado")), ms));
18  return Promise.race([promesa, limite]);
19}
20
21async function conReintentos(recurso) {
22  let motivo = "";
23  for (let intento = 1; intento <= INTENTOS; intento++) {
24    try {
25      await conTiempoLimite(peticion(recurso, intento), LIMITE_MS);
26      return { estado: "cargado", intentos: intento };
27    } catch (error) {
28      motivo = error.message;
29    }
30  }
31  return { estado: "error", motivo };
32}
33
34const recursos = new Map();
35lineas.forEach((linea, i) => {
36  const texto = linea.trim();
37  if (!texto) return;
38  const m = texto.match(/^(\S+)\s+(\d+)\s+(\d+)(?:\s+(\S+))?$/);
39  if (!m || recursos.has(m[1])) {
40    console.log(`Aviso: línea ${i + 1} ignorada`);
41    return;
42  }
43  recursos.set(m[1], { nombre: m[1], ms: Number(m[2]), fallos: Number(m[3]), depende: m[4] ? m[4].split(",") : [] });
44});
45
46// Cada recurso se carga una sola vez: se guarda su promesa y quien dependa de él la espera
47const cargas = new Map();
48function cargar(nombre) {
49  if (!cargas.has(nombre)) {
50    cargas.set(nombre, (async () => {
51      const recurso = recursos.get(nombre);
52      const deps = await Promise.all(recurso.depende.map((d) => (recursos.has(d) ? cargar(d) : { estado: "no existe" })));
53      const i = deps.findIndex((r) => r.estado !== "cargado");
54      if (i >= 0) {
55        const razon = deps[i].estado === "no existe" ? "que no existe" : "que no se ha podido cargar";
56        return { estado: "omitido", motivo: `depende de ${recurso.depende[i]}, ${razon}` };
57      }
58      return conReintentos(recurso);
59    })());
60  }
61  return cargas.get(nombre);
62}
63
64async function main() {
65  // Todas las cargas empiezan a la vez; Promise.all espera a que terminen todas
66  const resultados = await Promise.all([...recursos.keys()].map(cargar));
67  [...recursos.keys()].forEach((nombre, i) => {
68    const r = resultados[i];
69    if (r.estado === "cargado") console.log(`${nombre}: cargado (${r.intentos} ${r.intentos === 1 ? "intento" : "intentos"})`);
70    else if (r.estado === "error") console.log(`${nombre}: error tras ${INTENTOS} intentos (${r.motivo})`);
71    else console.log(`${nombre}: omitido (${r.motivo})`);
72  });
73  const cargados = resultados.filter((r) => r.estado === "cargado").length;
74  console.log(`Cargados: ${cargados} de ${resultados.length} · Peticiones: ${peticiones}`);
75}
76
77main();

Cada pieza resuelve un problema distinto de la asincronía: Promise.race pone un límite de tiempo, el bucle con await reintenta, Promise.all espera a varias cosas a la vez y el Map de promesas evita trabajo repetido. Combinadas dan un cargador robusto con muy poco código.

Guardar la promesa (no el resultado) es la clave de las dependencias: quien llegue antes de que termine la carga espera a la misma promesa, y quien llegue después recibe el resultado al instante. Es la técnica que usan las cachés de peticiones de las librerías de datos.

Los errores asíncronos se capturan con try/catch alrededor del await, igual que los síncronos. Los recursos que no se pueden cargar no rompen nada: se convierten en un resultado con estado error u omitido, y el resto del panel sigue funcionando.

El informe no depende de los tiempos: los resultados se muestran en el orden de la entrada porque Promise.all conserva el orden. Es la diferencia entre un programa que funciona «casi siempre» y uno que funciona siempre.

Para ir más allá

  • Limita a 2 las peticiones simultáneas con una cola, como hacen los navegadores con las conexiones a un mismo servidor.
  • Espera entre reintentos el doble cada vez (10, 20, 40 ms): el «retroceso exponencial» que usan los clientes de las APIs reales.
  • Sustituye la simulación por fetch contra una API pública y cancela las peticiones lentas con AbortController.

Dónde se explica