Async Map Timeout


Mi 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.

Mi 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.

Mi 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 🏃‍♂️