Tracks
/
JavaScript
JavaScript
/
Ejercicios
/
Servicio de traducción
Servicio de traducción

Servicio de traducción

Ejercicio de aprendizaje

Introducción

El objeto Promise representa la finalización (o el fallo) eventual de una operación asíncrona y su valor resultante.

Note

Este es un tema difícil para muchas personas, sobre todo si sabes programar en un lenguaje que es completamente síncrono. Si te resulta abrumador, o quieres aprender más sobre la concurrencia y el paralelismo, mira (en go.dev) o mira directamente en Vimeo y lee las diapositivas de la brillante charla «Concurrency is not parallelism».

Ciclo de vida de una promesa

Una Promise tiene tres estados:

  1. pendiente
  2. resuelta
  3. rechazada

Cuando se crea, una promesa está pendiente. En algún momento futuro puede resolverse o rechazarse. Una vez que una promesa se resuelve o se rechaza, no puede volver a resolverse ni rechazarse, y su estado tampoco puede cambiar.

En otras palabras:

  1. Mientras está pendiente, una promesa:
    • puede pasar al estado resuelta o rechazada.
  2. Cuando está resuelta, una promesa:
    • no debe pasar a ningún otro estado.
    • debe tener un valor, que no debe cambiar.
  3. Cuando está rechazada, una promesa:
    • no debe pasar a ningún otro estado.
    • debe tener una razón, que no debe cambiar.

Cómo resolver una promesa

Una promesa puede resolverse de varias 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;
});

En los ejemplos anteriores, value puede ser cualquier cosa, incluido un error, undefined, null u otra promesa. Por lo general, querrás resolver con un valor que no sea un error.

Cómo rechazar una promesa

Una promesa puede rechazarse de varias 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
})

En los ejemplos anteriores, reason puede ser cualquier cosa, incluido un error, undefined o null. Por lo general, querrás rechazar con un error.

Cómo encadenar una promesa

Una promesa puede continuarse con una acción futura una vez que se resuelve o se rechaza.

then

Toda promesa es «thenable». Eso significa que hay una función then disponible que se ejecutará una vez que la promesa original se resuelva. Dado promise.then(onResolved), el callback onResolved recibe el valor con el que se resolvió la promesa original. Esto siempre devolverá una nueva promesa «encadenada».

Devolver un value desde then resuelve la promesa «encadenada». Lanzar un reason en then rechaza la promesa «encadenada».

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;
});

Esto imprimirá "Success!" en la consola después de aproximadamente 1000 ms. El estado y el valor de promise1 serán resolved y "Success!". El estado y el valor de promise2 serán resolved y true.

Hay un segundo argumento disponible que se ejecuta cuando la promesa original se rechaza. Dado promise.then(onResolved, onRejected), el callback onResolved recibe el valor con el que se resolvió la promesa original, o el callback onRejected recibe la razón por la que se rechazó la promesa.

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);
  • En aproximadamente la mitad de los casos, esto imprimirá "Success!" en la consola después de aproximadamente 1000 ms.
    • El estado y el valor de promise1 serán resolved y "Success!".
    • El estado y el valor de promise2 serán resolved y true.
  • En aproximadamente la mitad de los casos, esto imprimirá inmediatamente "NOPE!" en la consola.
    • El estado y el valor de promise1 serán rejected y Nope!.
    • El estado y el valor de promise2 serán resolved y false.

Es importante entender que, debido a las reglas del ciclo de vida, cuando se llama a reject, el resolve que llega unos 1000 ms más tarde se ignora en silencio, ya que el estado interno no puede cambiar una vez que se ha rechazado o resuelto. Es importante entender que devolver un valor desde una promesa la resuelve, y lanzar un valor la rechaza. Cuando promise1 se resuelve y hay un onResolved encadenado: then(onResolved), esa continuación es una nueva promesa que puede resolverse o rechazarse. Cuando promise1 se rechaza pero hay un onRejected encadenado: then(, onRejected), esa continuación es una nueva promesa que puede resolverse o rechazarse.

catch

A veces quieres capturar errores y solo continuar cuando la promesa original se rechaza. Dado promise.catch(onCatch), el callback onCatch recibe la razón por la que se rechazó la promesa original. Esto siempre devolverá una nueva promesa «encadenada».

Devolver un value desde catch resuelve la promesa «encadenada». Lanzar un reason en catch rechaza la promesa «encadenada».

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);

En aproximadamente la mitad de los casos, esto imprimirá "Success!" en la consola después de aproximadamente 1000 ms. En la otra mitad de los casos, esto imprimirá inmediatamente 42 en la consola.

  • Si promise1 se resuelve, se omite catch y se llega a then, que imprime el valor.
    • El estado y el valor de promise1 serán resolved y "Success!".
    • El estado y el valor de promise2 serán resolved y "done";
  • Si promise1 se rechaza, se ejecuta catch, que devuelve un valor, y por lo tanto la cadena ahora está resolved, y se llega a then, que imprime el valor.
    • El estado y el valor de promise1 serán rejected y "Nope!".
    • El estado y el valor de promise2 serán resolved y "done";

finally

A veces quieres ejecutar código después de que una promesa finalice, sin importar si se resuelve o se rechaza. Dado promise.finally(onSettled), el callback onSettled no recibe nada. Esto siempre devolverá una nueva promesa «encadenada».

Devolver un value desde finally copia el estado y el valor de la promesa original, ignorando el value. Lanzar un reason en finally rechaza la promesa «encadenada», sobrescribiendo cualquier estado y valor o razón de la promesa original.

Ejemplo

Varios de los 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');
  });
  • En los casos en que randomNumber es 0-3:
    • myPromise se resolverá con el valor 2, 4, 6, or 8
    • finalPromise se resolverá con el valor 'yay'
    • Habrá dos mensajes en la consola:
      • Sampled data: ...
      • Promise completed
  • En los casos en que randomNumber es 4-5:
    • myPromise se rechazará con la razón 'Sampling did not result in a sample'
    • finalPromise se rechazará con la razón Error('Sampling did not result in a sample')
    • Habrá un mensaje en la consola:
      • Promise completed
      • en algunos entornos esto producirá un registro "uncaught rejected promise: Error('Sampling did not result in a sample')"

Como se muestra arriba, reject funciona con un string, y una promesa también puede rechazarse con un Error.

Note

Si encadenar promesas o el uso general no te queda claro, el tutorial de MDN es un buen recurso para consultar.

Instrucciones

En este ejercicio, vas a implementar un TranslationService que brinda servicios básicos de traducción a los miembros gratuitos, y traducción avanzada, con garantía de calidad, a los miembros premium.

La API

Encontraste una API de traducción del espacio exterior que atiende cualquier request de traducción en un tiempo razonable. Quieres sacarle provecho. Los traductores espaciales son extremadamente caprichosos y odian la redundancia, así que también ofrecen satélites de almacenamiento de la API donde puedes hacer fetch de traducciones anteriores sin molestarlos.

Obtener una traducción

api.fetch(text) obtiene una traducción de text del almacenamiento de la API y devuelve una promise que entrega dos valores:

  • translation: la traducción en sí
  • quality: la calidad expresada como un número

Si no se encuentra una traducción en el almacenamiento de la API, la API lanza un error NotAvailable. Las traducciones se pueden agregar con el método api.request. Si «text» no se puede traducir, la API lanza un error Untranslatable.

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

Solicitar una traducción

Algunas traducciones existen con seguridad, pero todavía no se han agregado al almacenamiento de la API. Esa es la diferencia entre NotAvailable (no está en el almacenamiento, pero se puede solicitar) y Untranslatable (no se puede traducir).

api.request(text, callback) solicita que se realice una traducción de text y se agregue al almacenamiento de la API. Al terminar, se llama a la función callback.

  • Si tiene éxito, se le pasa undefined a callback: esto indica que la traducción se realizó correctamente y se puede acceder a ella con el método api.fetch.
  • Si falla, se le pasa un error a callback: esto indica que algo salió mal. La API del espacio exterior es inestable, lo que significa que la API falla a menudo. Si eso pasa, está bien volver a llamar a api.request.
api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
//        because it was successful.

⚠ ¡Advertencia! ⚠

Caution

La API hace su magia teletransportando a los distintos traductores cuando llega un request. Esta es una acción muy costosa, así que no debería llamarse cuando una traducción sí está disponible. Lamentablemente, no todos leen el manual, así que hay un sistema para expulsar a los que abusan del servicio.

Si se llama a api.request para un text que está disponible, la API lanza un AbusiveClientError para esa llamada, y para todas las llamadas posteriores. Asegúrate de nunca solicitar una traducción si algo ya se ha traducido.

1. Obtener una traducción, sin tener en cuenta la calidad

El servicio gratuito solo ofrece traducciones que ya están en el almacenamiento de la API.

Implementa un método free(text) que entregue a los miembros gratuitos traducciones que ya existen en el almacenamiento de la API. Ignora la calidad y propaga cualquier error que lance la API.

  • Devuelve la traducción si se puede recuperar, sin importar su calidad
  • Propaga cualquier error de la API de traducción
  • Usa el método api.fetch (api.fetch devuelve una promise)
service.free('jIyaj');
// => Promise<...> resolves "I understand."

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

2. Obtener un lote de traducciones, todo o nada

Implementa un método batch([text, text, ...]) para los miembros gratuitos que traduzca un array de textos con el servicio gratuito y devuelva todas las traducciones, o un solo error.

  • Se resuelve con todas las traducciones (en el mismo orden), si todas están disponibles
  • Se rechaza con el primer error que se encuentre
  • Se rechaza con un error BatchIsEmpty si no se proporciona ningún 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. Solicitar una traducción, con un máximo de 2 reintentos

Implementa un método request(text) para usuarios premium, que solicita que se agregue una traducción al almacenamiento de la API. La solicitud debe reintentarse automáticamente si ocurre un fallo. No debe hacer más de 3 llamadas para la misma solicitud (¡no hagas enojar a los traductores espaciales!).

  • Si api.request no devuelve un error, resuelve con undefined
  • Si api.request devuelve un error, reintenta como máximo dos veces
  • Si se te acaban los reintentos, rechaza con el último error recibido
service.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API

4. Obtener una traducción, inspeccionar la calidad o solicitarla

Implementa un método premium(text, quality) para usuarios premium que obtenga una traducción. Si una traducción está NotAvailable, solicita la traducción y óbtenla después de que se haya agregado al almacenamiento de la API. El método solo debe devolver la traducción si cumple con un umbral de quality determinado.

  • Si api.fetch se resuelve, verifica la calidad antes de resolver
  • Si api.fetch se rechaza, solicita la traducción en su lugar
  • Si api.request se rechaza, propaga el error
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?"

N.B.

Note

La traducción correcta de 'arlogh Qoylu'pu'? es How many times has it been heard?.

Editar en GitHub El enlace se abre en una ventana o una pestaña nuevas
JavaScript Exercism

¿Todo listo para empezar Servicio de traducción?

Regístrate en Exercism para aprender y dominar JavaScript con 37 conceptos159 ejercicios y mentoría humana real, todo gratis.