Cargador de datos asíncrono con reintentos y dependencias
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
- Cada línea es
nombre ms fallos [dependencias]: lo que tarda cada petición en milisegundos, cuántas de las primeras peticiones fallan conerror del servidory, 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 escribeAviso: línea N ignorada. No hay dependencias circulares. - 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.
- Cada intento dura lo que diga
ms, pero si pasan 100 ms sin respuesta se da por fallido contiempo agotado. Se hacen hasta 3 intentos, uno detrás de otro, y el motivo de error que se informa es el del último. - 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)unombre: 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. - 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.
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.
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.
Ejemplo
usuarios 30 0 productos 20 2 fotos 150 0 pedidos 10 0 usuarios,productos galeria 10 0 fotos informe 10 0 pedidos,stock
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
Solución explicada
Ver la solución completa
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
fetchcontra una API pública y cancela las peticiones lentas conAbortController.