Problema
En el post sobre AsyncMap vimos cómo procesar colecciones de forma secuencial asíncrona. Sin embargo, al trabajar con APIs externas que implementan rate limiting, nos enfrentamos a un problema crítico:
// Procesar 1000 URLs secuencialmente
let results = try await urls.asyncMap { url in
try await apiClient.fetch(url) // ⚠️ 1000 llamadas sin pausa
}
// Error 429: Too Many Requests
Realizar llamadas consecutivas sin pausas puede:
- Exceder límites de rate: APIs rechazan con 429 Too Many Requests.
- Saturar servicios externos: sobrecarga de conexiones simultáneas.
- Desperdiciar recursos: forzar reintentos consume más tiempo y ancho de banda.
- Bloqueos temporales: algunas APIs bloquean la IP tras múltiples infracciones.
Necesitamos una forma de controlar el ritmo de las operaciones secuenciales, añadiendo pausas intencionales entre cada procesamiento.
Solución
Actualizamos asyncMap añadiendo un parámetro opcional timeout que introduce una pausa configurable después de procesar cada elemento.
extension Sequence {
func asyncMap<T>(
timeout: Double? = nil,
_ transform: (Element) async throws -> T
) async throws -> [T] {
var results = [T]()
results.reserveCapacity(underestimatedCount)
for element in self {
try await results.append(transform(element))
if let timeout {
try await Task.sleep(for: .seconds(timeout))
}
}
return results
}
}
Cambios clave respecto a la versión original:
✨ Parámetro timeout: Double? = nil opcional y retrocompatible.
⏱️ Si se especifica timeout, añade Task.sleep(for: .seconds(timeout)) después de cada elemento.
🔄 Mantiene el comportamiento original cuando no se especifica timeout (sin pausas).
📊 Permite ajustar dinámicamente el rate limiting según los límites de cada API.
Resultado
// Rate limiting: 1 llamada por segundo
let results = try await urls.asyncMap(timeout: 1.0) {
try await apiClient.fetch($0)
}
// Rate limiting agresivo: 1 llamada cada 5 segundos
let results = try await endpoints.asyncMap(timeout: 5.0) {
try await scraper.parse($0)
}
// Sin timeout: comportamiento original (máxima velocidad)
let results = try await localFiles.asyncMap {
try await processFile($0)
}
Beneficios de esta actualización:
⏱️ Rate limiting configurable: controla el ritmo de llamadas según los límites de cada API.
🛡️ Prevención de bloqueos: evita errores 429 y suspensiones temporales de IP.
🔄 Retrocompatibilidad total: sin timeout funciona exactamente igual que antes.
🎯 Flexibilidad por caso de uso: ajusta el timeout según la tolerancia del servicio externo.
📊 Procesamiento predecible: calcula fácilmente el tiempo total (n elementos × timeout).
Esta actualización convierte asyncMap en una herramienta completa para procesamiento secuencial controlado, ideal para integración con APIs que imponen límites de tasa y necesitan un flujo de peticiones regulado.
Keep coding, keep running 🏃♂️