Como lidar com várias esperas na função assíncrona

8

Tenho várias chamadas de API a serem feitas, que buscam por uma API, gravam dados no DB via API, enviam a saída para o front-end por outra API.

Eu escrevi função assíncrona com aguardar como abaixo -

Os dois primeiros devem ser executados um após o outro, mas o terceiro pode ser executado de forma independente e não é necessário aguardar a conclusão das duas primeiras instruções de busca.

let getToken= await fetch(url_for_getToken);
let getTokenData = await getToken.json();

let writeToDB = await fetch(url_for_writeToDB);
let writeToDBData = await writeToDB.json();

let frontEnd = await fetch(url_for_frontEnd);
let frontEndData = await frontEnd.json();

Qual é a melhor maneira de lidar com essas múltiplas instruções de busca?

Yasar Abdullah
fonte
6
Você pode dar uma olhada no Promise.all .
Yannick K
2
@YannickK Promise.all seria necessário aqui? Ele não poderia simplesmente usar .then ()? Ele não está esperando a conclusão de ambos, mas o primeiro, depois o segundo e o terceiro, independentemente dos dois.
Kobe
1
@Kobe Acho que a principal questão neste caso é que OP quer separar as chamadas de servidor e cliente, uma vez que não são dependentes um do outro - e seria tolo em termos de performance se eles esperou uns sobre os outros - mas se qualquer dos eles falham, você quer uma rejeição. Você está definitivamente certo de que ele poderia ficar sem Promise.all, mas neste caso, eu imagino que seria mais limpo (e mais fácil de construir no futuro) se ele encerrasse tudo em uma Promise.allchamada, especificamente para tratamento de erros.
Yannick K
@Kobe Porque Promise.allé essencial para o tratamento adequado dos erros e aguardar a conclusão das promessas de primeiro, segundo e terceiro.
Bergi 12/11/19
1
A resposta mais simples resolve melhor o problema, mas infelizmente foi prejudicada sem merecimento. Vale a pena tentar, @Yasar Abdulllah.
AndreasPizsa 12/11/19

Respostas:

2

Existem várias maneiras, mas a mais universal é agrupar cada caminho de código assíncrono em uma função assíncrona. Isso permite flexibilidade de misturar e combinar valores de retorno assíncronos, como desejar. No seu exemplo, você pode até embutir código com o iyn assíncrono:

await Promise.all([
  (async() => {
    let getToken = await fetch(url_for_getToken);
    let getTokenData = await getToken.json();

    let writeToDB = await fetch(url_for_writeToDB);
    let writeToDBData = await writeToDB.json();
  })(),
  (async() => {
    let frontEnd = await fetch(url_for_frontEnd);
    let frontEndData = await frontEnd.json();
  })()
]);
Marzelin
fonte
Finalmente, uma resposta adequada! : D
Bergi 12/11/19
1

Você pode usar .then(), em vez de esperar:

fetch(url_for_getToken)
  .then(getToken => getToken.json())
  .then(async getTokenData => {
    let writeToDB = await fetch(url_for_writeToDB);
    let writeToDBData = await writeToDB.json();
    // Carry on
  })

fetch(url_for_frontEnd)
  .then(frontEnd => frontEnd.json())
  .then(frontEndData => {
    // Carry on  
  })
Kobe
fonte
1
Não se esqueça de lidar com erros nessas cadeias de promessas - ou Promise.allneles e devolvê-los ao chamador.
Bergi 12/11/19
1

É mais fácil se você trabalhar com "criadores" promissores (= função que retorna promessas) em vez de promessas brutas. Primeiro, defina:

const fetchJson = (url, opts) => () => fetch(url, opts).then(r => r.json())

que retorna um "criador". Agora, aqui estão dois utilitários para encadeamento serial e paralelo, que aceitam promessas brutas e "criadores":

const call = f => typeof f === 'function' ? f() : f;

const parallel = (...fns)  => Promise.all(fns.map(call));

async function series(...fns) {
    let res = [];

    for (let f of fns)
        res.push(await call(f));

    return res;
}

Então, o código principal pode ser escrito assim:

let [[getTokenData, writeToDBData], frontEndData] = await parallel(
    series(
        fetchJson(url_for_getToken),
        fetchJson(url_for_writeToDB),
    ),
    fetchJson(url_for_frontEnd),
)

Se você não gostar do wrapper "criador" dedicado, poderá definir fetchJsonnormalmente

const fetchJson = (url, opts) => fetch(url, opts).then(r => r.json())

e use continuações em linha exatamente onde seriesou parallelsão chamadas:

let [[getTokenData, writeToDBData], frontEndData] = await parallel(
    series(
        () => fetchJson('getToken'),
        () => fetchJson('writeToDB'),
    ),
    () => fetchJson('frontEnd'), // continuation not necessary, but looks nicer
)

Para levar a idéia adiante, podemos criar seriese paralleldevolver "criadores", em vez de promessas. Dessa forma, podemos construir "circuitos" aninhados arbitrários de promessas seriais e paralelas e obter os resultados em ordem. Exemplo de trabalho completo:

const call = f => typeof f === 'function' ? f() : f;

const parallel = (...fns)  => () => Promise.all(fns.map(call));

const series = (...fns) => async () => {
    let res = [];

    for (let f of fns)
        res.push(await call(f));

    return res;
};

//

const request = (x, time) => () => new Promise(resolve => {
    console.log('start', x);
    setTimeout(() => {
        console.log('end', x)
        resolve(x)
    }, time)
});

async function main() {
    let chain = series(
        parallel(
            series(
                request('A1', 500),
                request('A2', 200),
            ),
            series(
                request('B1', 900),
                request('B2', 400),
                request('B3', 400),
            ),
        ),
        parallel(
            request('C1', 800),
            series(
                request('C2', 100),
                request('C3', 100),
            )
        ),
    );

    let results = await chain();

    console.log(JSON.stringify(results))
}

main()
.as-console-wrapper { max-height: 100% !important; top: 0; }

georg
fonte
por que você simplesmente não retorna a promessa fetchJson? Não vejo nenhum benefício em cumprir uma promessa com outra função. E a utilidade de seriesé questionável, pois a próxima função de uma série normalmente requer o valor de retorno das funções anteriores.
marzelin
@ marzelin: as promessas começam imediatamente na criação, portanto, series(promiseA, promiseB)serão iniciadas Ae Bao mesmo tempo.
Georg
1
Ah sim. Toda essa cerimônia apenas para usar series. Prefiro preferir uma vida assíncrona para sequências e Promise.allSettlednão parallel.
marzelin
@ Marzelin: Acho essa sintaxe mais limpa (consulte a atualização).
21819 georg #
1
@ KamilKiełczewski: Atualizei o post ... A idéia é que essa sintaxe de continuação permita a criação de "fluxos" de promessa aninhados sem muito aborrecimento.
22919 georg
-2

Execute request independente ( writeToDB) no início e semawait

let writeToDB = fetch(url_for_writeToDB);

let getToken = await fetch(url_for_getToken);
let getTokenData = await getToken.json();

// ...

Kamil Kiełczewski
fonte
1
@Bergi a questão não é sobre manipulação de erro - manipulação de erro (como try-catch) é importante, mas é tópico separado
Kamil Kiełczewski
awaitpossui tratamento de erros embutido - quando a promessa rejeita, você recebe uma exceção, a promessa retornada pela async functionchamada será rejeitada e o chamador notará. Se você sugerir descartar a awaitpalavra - chave e executar a cadeia de promessas de forma independente, não deve fazê-lo sem pensar em manipulação de erros. Uma resposta que pelo menos não a menciona (ou preserva o comportamento original do chamador recebendo uma rejeição) é uma resposta ruim.
Bergi 12/11/19
1
Essa resposta resolve o problema do OP e também é a mais simples de todas as respostas apresentadas até o momento. Estou curioso para saber por que isso foi rebaixado? O OP declarou " o terceiro pode ser executado de forma independente e não é necessário aguardar a conclusão das duas primeiras instruções de busca " . Portanto, executá-lo primeiro é bom.
AndreasPizsa
@AndreasPizsa É muito simples . Eu não teria votado mal se houvesse pelo menos um aviso de que os erros não são tratados - assim que é mencionado, qualquer pessoa pode fazer sua própria opinião sobre se é ou não apropriado para o seu caso - pode até ser para o OP . Mas ignorar isso leva aos piores erros de todos e torna essa resposta inútil.
Bergi 12/11/19
1
Obrigado por comentar @Bergi. Dado que a pergunta do OP também não incluía o tratamento de erros, eu diria que está sendo deixado de fora por brevidade e além do escopo da pergunta. Mas sim, qualquer pessoa pode votar com sua própria opinião.
AndreasPizsa