Percursos
/
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 que dela resulta.

Note

Este é um tema difícil para muita gente, sobretudo se sabes programar numa linguagem completamente síncrona. Se te sentires sobrecarregado, ou se quiseres saber mais sobre concorrência e paralelismo, vê (via go.dev) ou vê diretamente no vimeo e lê os diapositivos da brilhante palestra "Concurrency is not parallelism".

Ciclo de vida de uma promise

Uma Promise tem três estados:

  1. pendente
  2. cumprida
  3. rejeitada

Quando é criada, uma promise está pendente. Em algum momento no futuro, pode ser resolvida ou rejeitada. Depois de uma promise ser resolvida ou rejeitada uma vez, nunca mais pode voltar a ser resolvida nem rejeitada, e o seu estado também não pode mudar.

Por outras palavras:

  1. Quando está pendente, uma promise:
    • pode passar a estar cumprida ou rejeitada.
  2. Quando está cumprida, uma promise:
    • não pode passar a nenhum outro estado.
    • tem de ter um valor, que não pode mudar.
  3. Quando está rejeitada, uma promise:
    • não pode passar a nenhum outro estado.
    • tem de ter um motivo, que não pode mudar.

Resolver 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, queres resolver com um valor que não seja um erro.

Rejeitar 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, queres rejeitar com um erro.

Encadear uma promise

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

then

Todas as promises são "thenable". Isto significa que existe uma função then disponível que será executada assim que a promise original for resolvida. Dado promise.then(onResolved), a função de callback onResolved recebe o valor com que a promise original foi resolvida. Isto devolve sempre uma nova promise "encadeada".

Devolver um value a partir do then resolve a promise "encadeada". Lançar um reason no 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;
});

Isto regista "Success!" ao fim 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), a função de callback onResolved recebe o valor com que a promise original foi resolvida, ou a função de 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 metade dos casos, isto regista "Success!" ao fim 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 metade dos casos, isto regista 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 perceber que, devido às regras do ciclo de vida, quando há um reject, o resolve que chega cerca de 1000 ms depois é silenciosamente ignorado, porque o estado interno não pode mudar depois de a promise ser rejeitada ou resolvida. É importante perceber que devolver um valor a partir de uma promise faz com que esta seja resolvida, e que lançar um valor faz com que seja rejeitada. Quando promise1 é resolvida e existe um onResolved encadeado, then(onResolved), o que se segue é uma nova promise que pode ser resolvida ou rejeitada. Quando promise1 é rejeitada mas existe um onRejected encadeado, then(, onRejected), o que se segue é uma nova promise que pode ser resolvida ou rejeitada.

catch

Por vezes queres capturar erros e só continuar quando a promise original é rejeitada. Dado promise.catch(onCatch), a função de callback onCatch recebe o motivo pelo qual a promise original foi rejeitada. Isto devolve sempre uma nova promise "encadeada".

Devolver um value a partir do catch resolve a promise "encadeada". Lançar um reason no 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 metade dos casos, isto regista "Success!" ao fim de aproximadamente 1000 ms. Na outra metade dos casos, isto regista imediatamente 42.

  • Se promise1 for resolvida, o catch é ignorado e chega-se ao then, que regista 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, o catch é executado, o que devolve um valor, e portanto a cadeia fica agora resolved e chega ao then, que regista 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

Por vezes queres executar código depois de uma promise terminar, independentemente de ela ser resolvida ou rejeitada. Dado promise.finally(onSettled), a função de callback onSettled não recebe nada. Isto devolve sempre uma nova promise "encadeada".

Devolver um value a partir do finally copia o estado e o valor da promise original, ignorando o value. Lançar um reason no finally rejeita a promise "encadeada", substituindo qualquer estado e valor, ou motivo, da promise original.

Exemplo

Vários dos métodos em conjunto:

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 registos:
      • 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 registo:
      • Promise completed
      • em alguns ambientes, isto dará origem a um registo "uncaught rejected promise: Error('Sampling did not result in a sample')"

Como se mostra acima, o reject funciona com uma string, e uma promise também pode ser rejeitada com um Error.

Note

Se encadear promises ou a utilização geral não for clara, o tutorial na MDN é um bom recurso para consultar.

Instruções

Neste exercício, vais criar um TranslationService que oferece serviços básicos de tradução aos membros gratuitos e tradução avançada aos membros premium, com garantias de qualidade.

A API

Encontraste uma API de tradução do espaço sideral que satisfaz qualquer request de tradução num tempo razoável. Queres tirar partido disso. Os tradutores espaciais são extremamente inconstantes e odeiam redundância, por isso também disponibilizam satélites de API storage onde podes fazer fetch de traduções antigas sem os incomodar.

Obter uma tradução

api.fetch(text) obtém uma tradução de text a partir do API storage e devolve uma promise que fornece dois valores:

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

Se uma tradução não for encontrada no API storage, a API lança um erro NotAvailable. As traduções podem ser adicionadas com o método api.request. Se 'text' não for traduzível, a API lança um erro Untranslatable.

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

Pedir uma tradução

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

api.request(text, callback) pede que uma tradução de text seja efetuada e adicionada ao API storage. Quando termina, a função callback é chamada.

  • Em caso de sucesso, é passado undefined à callback: isto indica que a tradução foi bem-sucedida e está acessível através do método api.fetch.
  • Em caso de falha, é passado um error à callback: isto indica que algo correu mal. A API do espaço sideral é instável, o que significa que falha com frequência. Se isso acontecer, podes voltar a chamar api.request.
api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
//        because it was successful.

⚠ Aviso! ⚠

Caution

A API faz a sua magia teletransportando os vários tradutores quando chega um request. É uma ação muito dispendiosa, por isso não deve ser chamada quando existe uma tradução disponível. Infelizmente, nem toda a gente lê o manual, por isso há um sistema para expulsar os maus atores.

Se for chamado um api.request para um text que já está disponível, a API lança um AbusiveClientError nesta chamada e em todas as chamadas seguintes. Certifica-te de que nunca pedes uma tradução se algo já foi traduzido.

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

O serviço gratuito só disponibiliza traduções que estão atualmente no API storage.

Implementa um método free(text) que dá aos membros gratuitos as traduções que já existem no API storage. Ignora a qualidade e propaga os erros lançados pela API.

  • Devolve a tradução se a conseguir obter, independentemente da qualidade
  • Propaga qualquer erro da API de tradução
  • Usa o método api.fetch (api.fetch devolve uma promise)
service.free('jIyaj');
// => Promise<...> resolves "I understand."

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

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

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

  • Resolve com todas as traduções (pela mesma ordem), se estiverem todas disponíveis
  • Rejeita com o primeiro erro que encontrar
  • Rejeita com um erro BatchIsEmpty se não for dado nenhum texto
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. Pedir uma tradução, tentando de novo no máximo 2 vezes

Implementa um método request(text) para utilizadores premium, que pede que uma tradução seja adicionada ao API storage. O pedido deve tentar de novo automaticamente se ocorrer uma falha. Não deve fazer mais de 3 chamadas para o mesmo pedido (não irrites os tradutores espaciais!!!).

  • Se api.request não devolver um erro, resolve com undefined
  • Se api.request devolver um erro, tenta de novo no máximo duas vezes
  • Se esgotares as tentativas, rejeita com o último erro recebido
service.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API

4. Obter uma tradução, inspecionar a qualidade ou pedi-la

Implementa um método premium(text, quality) para utilizadores premium que obtém uma tradução. Se uma tradução for NotAvailable, pede a tradução e obtém-na depois de ter sido adicionada ao API storage. O método só deve devolver a tradução se esta cumprir um determinado limite de quality.

  • Se api.fetch resolver, verifica a qualidade antes de resolver
  • Se api.fetch rejeitar, pede a tradução em vez disso
  • Se api.request rejeitar, propaga 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?"

Nota

Note

A tradução correta de 'arlogh Qoylu'pu'? é How many times has it been heard?.

Editar via GitHub A ligação abre numa nova janela ou separador
JavaScript Exercism

Estás pronto para começar Serviço de tradução?

Inscreve-te no Exercism para aprenderes e dominares JavaScript com 37 conceitos159 exercícios, e mentoria humana real, tudo grátis.