Pulsas Guardar una sola vez. La petición falla temporalmente, vuelve a intentarlo y acaba con éxito. Sin embargo, aparecen dos notas idénticas. ¿De dónde salió la segunda?
Supongamos un pequeño servicio que crea una nota por petición. Este ejemplo independiente de JavaScript pierde una respuesta después de escribir y compara dos clientes que llaman al mismo servicio.
Coloca la escritura y el fallo en una misma secuencia
Abre el ejemplo descargable y lee el transporte antes del bucle de reintentos. Llama al servicio de notas, registra el ID devuelto y solo entonces lanza ResponseLost. Cuando el cliente recibe el error, el primer intento ya ha modificado los datos.
| Paso | Lo que ve el cliente | Notas guardadas |
|---|---|---|
| El primer intento escribe | Espera una respuesta | Una |
| El transporte pierde la respuesta | Un error | Una |
| El cliente vuelve a enviar | Espera otra vez | Depende de la clave y la consulta del servidor |
| Llega la segunda respuesta | Éxito | Una o dos |
El fallo está situado deliberadamente después de la escritura. Moverlo antes de la llamada probaría otro caso y ocultaría el duplicado. El transporte lanza errores de forma determinista, sin red ni temporizadores.
Crea la clave de la operación antes de entrar en el bucle
Los clientes solo difieren en el lugar donde generan la clave:
function saveUnsafe(send, title, newKey) {
return createWithRetry(send, title, () => newKey());
}
function save(send, title, newKey) {
const key = newKey();
return createWithRetry(send, title, () => key);
}
En saveUnsafe, el callback crea una clave nueva cada vez que el bucle envía la petición. El servicio interpreta que son dos operaciones y crea dos notas. En save, la clave se conserva antes de reintentar: ambos intentos llevan el mismo identificador y título.
El ejemplo usa cadenas numeradas para facilitar la comparación. Identifican una operación lógica de guardado, no cada intento de transporte ni el título. Dos guardados independientes con el mismo título reciben claves distintas. Revisa la duración del generador: una variable llamada «clave de idempotencia» también puede recrearse dentro de un envoltorio de reintentos.
Sigue la clave hasta la consulta del servidor
La clave estable funciona aquí porque el servicio consulta una entrada en completed antes de añadir la nota. La opción deduplicate, activada por defecto, permite que una prueba desactive esta consulta:
if (deduplicate && completed.has(key)) {
const previous = completed.get(key);
if (previous.title !== title)
throw new Error('key reused with another title');
return previous.note;
}
const note = Object.freeze({ id: `note-${notes.length + 1}`, title });
notes.push(note);
completed.set(key, { title, note });
return note;
Si se repiten la clave y el título, devuelve la nota creada anteriormente. Si la clave se repite con otro título, lanza un error antes de escribir de nuevo. El título es todo el contenido de la petición en este ejemplo, por lo que la comparación cubre la entrada completa.
La entrada conserva tanto los datos recibidos como el resultado. Examina el retorno anticipado: continuar hasta la inserción seguiría creando un duplicado. La prueba corregida comprueba el objeto devuelto y la cantidad final de notas. Otra reproducción desactiva la deduplicación en el servidor: aun manteniendo la clave en el cliente, aparecen dos notas.
Ejecuta los fallos y compara los datos guardados
Descarga retry-idempotency.mjs. Con Node.js 22 o posterior, ejecútalo sin instalar dependencias:
node --test retry-idempotency.mjs
Pasan las seis pruebas. Las dos cuyo nombre incluye «reproduce» pasan cuando ocurre el defecto intencionado. createWithRetry permite dos intentos en total y solo reintenta el error simulado de respuesta perdida; un conflicto de título se detiene tras un intento.
| Caso | Envíos | Notas guardadas | Resultado para quien llama |
|---|---|---|---|
| Nueva clave al reintentar | 2 | 2 | Segunda nota, note-2 |
| Misma clave y resultado reutilizable | 2 | 1 | Nota original, note-1 |
| Dos guardados independientes | 2 | 2 | Dos ID distintos |
| Otro título con una clave existente | 1 adicional | 1 en total | Rechazo |
| Se pierden ambas respuestas | 2 | 1 | Error después del segundo intento |
| Misma clave sin deduplicación | 2 | 2 | Segunda nota |
La quinta fila ayuda a leer el manejo de errores: el cliente termina con un error aunque la nota existe. Las aserciones examinan los datos guardados, las claves de los intentos y los ID devueltos. Comprobar solo una respuesta exitosa podría ocultar una escritura duplicada.
Recorre los mismos cinco puntos en una aplicación
- Encuentra la acción del usuario y decide qué intentos pertenecen a la misma operación.
- Localiza la creación de la clave y síguela por cada envoltorio de reintentos y argumento de la petición.
- Abre la escritura real y marca los fallos que pueden producirse después de completarla.
- Sigue la consulta del servidor, la comparación de entradas y el retorno de un resultado anterior.
- Contrasta el límite de intentos y la salida de error final con el estado guardado.
Anota las ubicaciones del código. Si un envoltorio vuelve a ejecutar un callback que genera la clave, lee también ese callback. Para seguir el recorrido completo desde la ruta hasta el almacenamiento, consulta cómo rastrear una petición HTTP en el código.
Separa el ejemplo en memoria de las garantías duraderas
Este servicio guarda todo en un Map y un array de un solo proceso. La consulta y la escritura son síncronas. No prueba transacciones de base de datos, workers concurrentes, reinicios ni recuperación persistente, y no ofrece una garantía de exactly-once en producción.
Una implementación real necesita decidir el ámbito de la clave, cómo compara las entradas, cuánto conserva los registros, qué hace con intentos concurrentes y cómo sobreviven juntos la escritura y el resultado guardado a un fallo. Este cliente también olvida la clave al terminar Guardar: un reintento manual posterior o una recarga necesitan otro diseño de duración de la operación. Revisa el almacenamiento y el cliente reales. La guía para leer código ayuda a mantener cerca las definiciones y los puntos de llamada mientras los sigues.