Requisições assíncronas consecutivas com Promise

Realizar requisições AJAX consecutivas é uma necessidade com a qual tenho me deparado de tempos em tempos e as soluções que criei tem melhorado e hoje estou satisfeito com o resultado.

O desafio é fazer uma requisição terminar antes da próxima ser enviada, para impedir que sejam enviadas várias requisições simultâneas que vão sobrecarregar o servidor ou causar um bloqueio temporário para a API que está sendo consumida.

No meu processo passei por várias etapas. No início percebi a inviabilidade de fazer “na mão” por haveriam níveis inaceitáveis de funções aninhadas. A primeira versão utilizava timeout com o tempo estimado de cada requisição. Em outra implementação usei um array de funções funcionando como uma fila FIFO (First In First Out), essa versão funcionou bem.

Estudando a API Fetch me vi obrigado a entender como promises funcionam e achei que seria uma forma elegante de resolver esse problema.

Primeiro definimos as URLs que serão consumidas:

let urls = [
    'http://exemplo.com.br/teste/123',
    'http://exemplo.com.br/teste/456',
    'http://exemplo.com.br/teste/789'
]

Como base para a execução consecutiva será usada uma promise já resolvida:

let promise = new Promise(resolve => resolve())

Para iterar as URLs é comum realizar um laço for mas ele não vai funcionar com código assíncrono, pois quando a função fetch for executada o valor da variável será o valor da última iteração do laço:

for (url of urls) promise.then(() => fetch(url))

Irá fazer a requisição para 'http://exemplo.com.br/teste/789' 3 vezes. Para evitar essa situação é possível usar uma abordagem funcional:

urls.forEach(url => {
    promise.then(() => fetch(url))
})

Mas assim todas as requisições serão enviadas ao mesmo tempo e é equivalente a:

promise.then(() => fetch(urls[0]))
promise.then(() => fetch(urls[1]))
promise.then(() => fetch(urls[2]))

Entretanto o objetivo é encadear da seguinte forma:

promise
    .then(() => fetch(urls[0]))
    .then(() => fetch(urls[1]))
    .then(() => fetch(urls[2]))

Então podemos sobrescrever promise com o resultado de .then():

urls.forEach(url => {
    promise = promise.then(() => fetch(url))
})

Mas eu prefiro usar .reduce():

urls.reduce((promise, url) => {
    return promise.then(() => fetch(url))
}, promise)

Entendendo os retornos

Note que todos os exemplos acima usam retorno implícito das arrow functions por isso escrevo a solução completa com o tratamento da resposta de fetch e explicação da inclusão ou omissão de return:

urls.reduce((promise, url) => {
    return promise.then(() => {
        return fetch(url).then(resp => {
            // essa função será executada antes do próximo fetch por causa do return
            resp.json().then(data => {
                // essa função será executada junto ao próximo fetch devido
                // ausência do return; a ordem não pode ser garantida
            })
        })
    })
}, new Promise(resolve => resolve()))

Por hoje é isso, até a próxima.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s