Trilhas
/
JavaScript
JavaScript
/
Exercícios
/
Serviço de tradução
Serviço de tradução

Serviço de tradução

Exercício de aprendizagem

Introdução

O objeto Promise representa a conclusão (ou falha) eventual de uma operação assíncrona e o valor resultante dela.

Note

Este é um tópico difícil para muitas pessoas, especialmente se você programa em uma linguagem completamente síncrona. Se você se sentir sobrecarregado, ou quiser aprender mais sobre concorrência e paralelismo, assista (via go.dev) ou assista diretamente pelo vimeo e leia os slides da brilhante palestra "Concurrency is not parallelism".

Ciclo de vida de uma promise

Uma Promise tem três estados:

  1. pendente
  2. resolvida
  3. rejeitada

Quando é criada, uma promise está pendente. Em algum momento no futuro, ela pode ser resolvida ou rejeitada. Depois que uma promise é resolvida ou rejeitada uma vez, ela nunca mais pode ser resolvida ou rejeitada, nem seu estado pode mudar.

Em outras palavras:

  1. Quando pendente, uma promise:
    • pode passar a estar resolvida ou rejeitada.
  2. Quando resolvida, uma promise:
    • não pode passar para nenhum outro estado.
    • precisa ter um valor, que não pode mudar.
  3. Quando rejeitada, uma promise:
    • não pode passar para nenhum outro estado.
    • precisa ter um motivo, que não pode mudar.

Resolvendo uma promise

Uma promise pode ser resolvida de várias formas:

// Creates a promise that is immediately resolved
Promise.resolve(value);

// Creates a promise that is immediately resolved
new Promise((resolve) => {
  resolve(value);
});

// Chaining a promise leads to a resolved promise
somePromise.then(() => {
  // ...
  return value;
});

Nos exemplos acima, value pode ser qualquer coisa, incluindo um erro, undefined, null ou outra promise. Normalmente, você quer resolver com um valor que não seja um erro.

Rejeitando uma promise

Uma promise pode ser rejeitada de várias formas:

// Creates a promise that is immediately rejected
Promise.reject(reason)

// Creates a promise that is immediately rejected
new Promise((_, reject) {
  reject(reason)
})

// Chaining a promise with an error leads to a rejected promise
somePromise.then(() => {
  // ...
  throw reason
})

Nos exemplos acima, reason pode ser qualquer coisa, incluindo um erro, undefined ou null. Normalmente, você quer rejeitar com um erro.

Encadeando uma promise

Uma promise pode ser continuada com uma ação futura assim que for resolvida ou rejeitada.

then

Toda promise é "thenable". Isso significa que existe uma função then disponível que será executada assim que a promise original for resolvida. Dado promise.then(onResolved), o callback onResolved recebe o valor com o qual a promise original foi resolvida. Isso sempre retorna uma nova promise "encadeada".

Retornar um value de then resolve a promise "encadeada". Lançar um reason em then rejeita a promise "encadeada".

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);
});

const promise2 = promise1.then(function (value) {
  console.log(value);
  // expected output: "Success!"

  return true;
});

Isso vai registrar "Success!" depois de aproximadamente 1000 ms. O estado e o valor de promise1 serão resolved e "Success!". O estado e o valor de promise2 serão resolved e true.

Há um segundo argumento disponível que é executado quando a promise original é rejeitada. Dado promise.then(onResolved, onRejected), o callback onResolved recebe o valor com o qual a promise original foi resolvida, ou o callback onRejected recebe o motivo pelo qual a promise foi rejeitada.

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);

  if (Math.random() < 0.5) {
    reject('Nope!');
  }
});

function log(value) {
  console.log(value);
  return true;
}

function shout(reason) {
  console.error(reason.toUpperCase());
  return false;
}

const promise2 = promise1.then(log, shout);
  • Em cerca de 1/2 dos casos, isso vai registrar "Success!" depois de aproximadamente 1000 ms.
    • O estado e o valor de promise1 serão resolved e "Success!".
    • O estado e o valor de promise2 serão resolved e true.
  • Em cerca de 1/2 dos casos, isso vai registrar imediatamente "NOPE!".
    • O estado e o valor de promise1 serão rejected e Nope!.
    • O estado e o valor de promise2 serão resolved e false.

É importante entender que, por causa das regras do ciclo de vida, quando ela faz reject, o resolve que chega cerca de ~1000ms depois é ignorado silenciosamente, já que o estado interno não pode mudar depois que ela foi rejeitada ou resolvida. É importante entender que retornar um valor de uma promise a resolve, e lançar um valor a rejeita. Quando promise1 é resolvida e há um onResolved encadeado, then(onResolved), esse seguimento é uma nova promise que pode ser resolvida ou rejeitada. Quando promise1 é rejeitada, mas há um onRejected encadeado, then(, onRejected), esse seguimento é uma nova promise que pode ser resolvida ou rejeitada.

catch

Às vezes você quer capturar erros e só continuar quando a promise original faz reject. Dado promise.catch(onCatch), o callback onCatch recebe o motivo pelo qual a promise original foi rejeitada. Isso sempre retorna uma nova promise "encadeada".

Retornar um value de catch resolve a promise "encadeada". Lançar um reason em catch rejeita a promise "encadeada".

const promise1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);

  if (Math.random() < 0.5) {
    reject('Nope!');
  }
});

function log(value) {
  console.log(value);
  return 'done';
}

function recover(reason) {
  console.error(reason.toUpperCase());
  return 42;
}

const promise2 = promise1.catch(recover).then(log);

Em cerca de 1/2 dos casos, isso vai registrar "Success!" depois de aproximadamente 1000 ms. Nos outros 1/2 dos casos, isso vai registrar imediatamente 42.

  • Se promise1 for resolvida, catch é ignorado e o fluxo chega a then, e registra o valor.
    • O estado e o valor de promise1 serão resolved e "Success!".
    • O estado e o valor de promise2 serão resolved e "done";
  • Se promise1 for rejeitada, catch é executado, o que retorna um valor, e assim a cadeia agora está resolved, e o fluxo chega a then, e registra o valor.
    • O estado e o valor de promise1 serão rejected e "Nope!".
    • O estado e o valor de promise2 serão resolved e "done";

finally

Às vezes você quer executar um código depois que uma promise é finalizada, independentemente de ela ser resolvida ou rejeitada. Dado promise.finally(onSettled), o callback onSettled não recebe nada. Isso sempre retorna uma nova promise "encadeada".

Retornar um value de finally copia o estado e o valor da promise original, ignorando o value. Lançar um reason em finally rejeita a promise "encadeada", sobrescrevendo qualquer estado e valor, ou motivo, da promise original.

Exemplo

Vários dos métodos juntos:

const myPromise = new Promise(function (resolve, reject) {
  const sampleData = [2, 4, 6, 8];
  const randomNumber = Math.round(Math.random() * 5);

  if (sampleData[randomNumber]) {
    resolve(sampleData[randomNumber]);
  } else {
    reject('Sampling did not result in a sample');
  }
});

const finalPromise = myPromise
  .then(function (sampled) {
    // If the random number was 0, 1, 2, or 3, this will be
    // reached and the number 2, 4, 6, or 8 will be logged.
    console.log(`Sampled data: ${sampled}`);
    return 'yay';
  })
  .catch(function (reason) {
    // If the random number was 4 or 5, this will be reached and
    // reason will be "An error occurred". The entire chain will
    // then reject with an Error with the reason as message.
    throw new Error(reason);
  })
  .finally(function () {
    // This will always log after either the sampled data is
    // logged or the error is raised.
    console.log('Promise completed');
  });
  • Nos casos em que randomNumber é 0-3:
    • myPromise será resolvida com o valor 2, 4, 6, or 8
    • finalPromise será resolvida com o valor 'yay'
    • Haverá dois registros:
      • Sampled data: ...
      • Promise completed
  • Nos casos em que randomNumber é 4-5:
    • myPromise será rejeitada com o motivo 'Sampling did not result in a sample'
    • finalPromise será rejeitada com o motivo Error('Sampling did not result in a sample')
    • Haverá um registro:
      • Promise completed
      • em alguns ambientes isso vai gerar um registro "uncaught rejected promise: Error('Sampling did not result in a sample')"

Como mostrado acima, reject funciona com uma string, e uma promise também pode rejeitar com um Error.

Note

Se encadear promises ou o uso geral não estiver claro, o tutorial no MDN é um bom recurso para consultar.

Instruções

Neste exercício, você vai implementar um TranslationService que oferece serviços básicos de tradução para membros gratuitos e tradução avançada para membros premium, com garantias de qualidade.

A API

Você encontrou uma API de tradução do espaço sideral que atende qualquer request de tradução em um tempo razoável. Você quer tirar proveito disso. Os tradutores espaciais são extremamente volúveis e odeiam redundância, então eles também oferecem satélites de armazenamento da API, onde você pode usar fetch para obter traduções anteriores sem incomodá-los.

Buscando uma tradução

api.fetch(text) busca uma tradução de text no armazenamento da API e retorna uma promise que fornece dois valores:

  • translation: a tradução em si
  • quality: a qualidade expressa como um número

Se uma tradução não for encontrada no armazenamento da API, a API lança um erro NotAvailable. É possível adicionar traduções usando o método api.request. Se o 'text' não puder ser traduzido, a API lança um erro Untranslatable.

api.fetch('jIyaj');
// => Promise({ resolved: 'I understand' })

Solicitando uma tradução

Algumas traduções certamente existem, mas ainda não foram adicionadas ao armazenamento da API. É essa a diferença entre NotAvailable ( não está no armazenamento, mas pode ser solicitada ) e Untranslatable ( não pode ser traduzida ).

api.request(text, callback) solicita que uma tradução de text seja feita e adicionada ao armazenamento da API. Ao terminar, a função callback é chamada.

  • Em caso de sucesso, undefined é passado para o callback: isso indica que a tradução foi bem-sucedida e pode ser acessada usando o método api.fetch.
  • Em caso de falha, um error é passado para o callback: isso indica que algo deu errado. A API do espaço sideral é instável, o que significa que a API falha com frequência. Se isso acontecer, tudo bem chamar api.request de novo.
api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
//        because it was successful.

⚠ Atenção! ⚠

Caution

A API faz sua mágica teleportando os vários tradutores quando chega um request. Essa é uma ação muito custosa, então ela não deveria ser chamada quando uma tradução está disponível. Infelizmente, nem todo mundo lê o manual, então existe um sistema para expulsar quem age de má-fé.

Se um api.request for chamado para um text que está disponível, a API lança um AbusiveClientError para essa chamada, e para todas as chamadas seguintes. Garanta que você nunca solicite uma tradução se algo já foi traduzido.

1. Busque uma tradução, ignorando a qualidade

O serviço gratuito só fornece traduções que já estão no armazenamento da API.

Implemente um método free(text) que oferece aos membros gratuitos as traduções que já existem no armazenamento da API. Ignore a qualidade e repasse qualquer erro lançado pela API.

  • Retorna a tradução se ela puder ser obtida, independentemente da qualidade
  • Repassa qualquer erro da API de tradução
  • Usa o método api.fetch (api.fetch retorna uma promise)
service.free('jIyaj');
// => Promise<...> resolves "I understand."

service.free("jIyajbe'");
// => Promise<...> rejects Error("Not yet translated")

2. Busque um lote de traduções, tudo ou nada

Implemente um método batch([text, text, ...]) para membros gratuitos que traduz um array de textos usando o serviço gratuito, retornando todas as traduções ou um único erro.

  • Resolve com todas as traduções (na mesma ordem), se todas estiverem disponíveis
  • Rejeita com o primeiro erro encontrado
  • Rejeita com um erro BatchIsEmpty se nenhum texto for passado
service.batch(['jIyaj', "majQa'"]);
// => Promise<...> resolves ["I understand.", "Well done!"]

service.batch(['jIyaj', "jIyajbe'"]);
// => Promise<...> rejects new Error("Not yet translated")

service.batch([]);
// => Promise<...> rejects BatchIsEmpty()

3. Solicite uma tradução, tentando novamente no máximo 2 vezes

Implemente um método request(text) para usuários premium que solicita que uma tradução seja adicionada ao armazenamento da API. A solicitação deve tentar novamente de forma automática se ocorrer uma falha. Ela não deve fazer mais de 3 chamadas para a mesma solicitação (não irrite os tradutores espaciais!!!).

  • Se api.request não retornar um erro, resolva com undefined
  • Se api.request retornar um erro, tente novamente no máximo duas vezes
  • Se as tentativas acabarem, rejeite com o último erro recebido
service.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API

4. Busque uma tradução, inspecione a qualidade ou solicite-a

Implemente um método premium(text, quality) para usuários premium que busca uma tradução. Se a tradução for NotAvailable, solicite-a e busque-a depois que ela tiver sido adicionada ao armazenamento da API. O método só deve retornar a tradução se ela atingir um determinado limite de quality.

  • Se api.fetch resolver, verifique a qualidade antes de resolver
  • Se api.fetch rejeitar, solicite a tradução
  • Se api.request rejeitar, repasse o erro
service.premium("jIyajbe'", 100);
// => Promise<...> resolves "I don't understand."

service.premium("'arlogh Qoylu'pu'?", 100);
// => Promise<...> rejects QualityThresholdNotMet()

service.premium("'arlogh Qoylu'pu'?", 40);
// => Promise<...> resolves "What time is it?"

Obs.

Note

The correct translation of 'arlogh Qoylu'pu'? is How many times has it been heard?.

Editar via GitHub O link abre em uma nova janela ou aba
JavaScript Exercism

Tudo pronto para começar Serviço de tradução?

Crie sua conta no Exercism para aprender e dominar JavaScript com 37 conceitos159 exercícios e mentoria humana de verdade, tudo de graça.