Parcours
/
JavaScript
JavaScript
/
Exercices
/
Service de traduction
Service de traduction

Service de traduction

Exercice d'apprentissage

Introduction

L'objet Promise représente l'achèvement (ou l'échec) éventuel d'une opération asynchrone et la valeur qui en résulte.

Note

C'est un sujet difficile pour beaucoup de gens, surtout si tu connais la programmation dans un langage complètement synchrone. Si tu te sens dépassé, ou si tu veux en apprendre plus sur la concurrence et le parallélisme, regarde (via go.dev) ou regarde directement sur vimeo et lis les diapositives de la brillante conférence « Concurrency is not parallelism ».

Cycle de vie d'une promesse

Une Promise possède trois états :

  1. pending
  2. fulfilled
  3. rejected

Lorsqu'elle est créée, une promesse est pending. À un moment donné dans le futur, elle peut être résolue ou rejetée. Une fois qu'une promesse a été résolue ou rejetée, elle ne peut plus jamais l'être, et son état ne peut plus changer.

Autrement dit :

  1. Quand elle est pending, une promesse :
    • peut passer à l'état fulfilled ou rejected.
  2. Quand elle est fulfilled, une promesse :
    • ne doit pas passer à un autre état.
    • doit avoir une valeur, qui ne doit pas changer.
  3. Quand elle est rejected, une promesse :
    • ne doit pas passer à un autre état.
    • doit avoir une raison, qui ne doit pas changer.

Résous une promesse

Une promesse peut être résolue de différentes manières :

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

Dans les exemples ci-dessus, value peut être n'importe quoi, y compris une erreur, undefined, null ou une autre promesse. En général, on veut résoudre avec une valeur qui n'est pas une erreur.

Rejette une promesse

Une promesse peut être rejetée de différentes manières :

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

Dans les exemples ci-dessus, reason peut être n'importe quoi, y compris une erreur, undefined ou null. En général, on veut rejeter avec une erreur.

Enchaîne une promesse

Une promesse peut être enchaînée avec une action future une fois qu'elle est résolue ou rejetée.

then

Toute promesse est « thenable ». Cela signifie qu'il existe une fonction then disponible, qui sera exécutée une fois que la promesse d'origine est résolue. Avec promise.then(onResolved), le callback onResolved reçoit la valeur avec laquelle la promesse d'origine a été résolue. Cela renvoie toujours une nouvelle promesse « enchaînée ».

Renvoyer une value depuis then résout la promesse « enchaînée ». Lever une reason dans then rejette la promesse « enchaînée ».

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

Cela affichera "Success!" dans la console après environ 1000 ms. L'état et la valeur de promise1 seront resolved et "Success!". L'état et la valeur de promise2 seront resolved et true.

Un second argument est disponible et s'exécute quand la promesse d'origine est rejetée. Avec promise.then(onResolved, onRejected), le callback onResolved reçoit la valeur avec laquelle la promesse d'origine a été résolue, ou le callback onRejected reçoit la raison pour laquelle la promesse a été rejetée.

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);
  • Dans environ la moitié des cas, cela affichera "Success!" dans la console après environ 1000 ms.
    • L'état et la valeur de promise1 seront resolved et "Success!".
    • L'état et la valeur de promise2 seront resolved et true.
  • Dans environ la moitié des cas, cela affichera immédiatement "NOPE!" dans la console.
    • L'état et la valeur de promise1 seront rejected et Nope!.
    • L'état et la valeur de promise2 seront resolved et false.

Il est important de comprendre qu'à cause des règles du cycle de vie, quand elle reject, le resolve qui arrive environ 1000 ms plus tard est silencieusement ignoré, car l'état interne ne peut plus changer une fois qu'elle a été rejetée ou résolue. Il est important de comprendre que renvoyer une valeur depuis une promesse la résout, et que lever une valeur la rejette. Quand promise1 est résolue et qu'un onResolved est enchaîné : then(onResolved), alors ce qui suit est une nouvelle promesse qui peut être résolue ou rejetée. Quand promise1 est rejetée mais qu'un onRejected est enchaîné : then(, onRejected), alors ce qui suit est une nouvelle promesse qui peut être résolue ou rejetée.

catch

Parfois, tu veux capturer les erreurs et ne continuer que lorsque la promesse d'origine reject. Avec promise.catch(onCatch), le callback onCatch reçoit la raison pour laquelle la promesse d'origine a été rejetée. Cela renvoie toujours une nouvelle promesse « enchaînée ».

Renvoyer une value depuis catch résout la promesse « enchaînée ». Lever une reason dans catch rejette la promesse « enchaînée ».

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

Dans environ la moitié des cas, cela affichera "Success!" dans la console après environ 1000 ms. Dans l'autre moitié des cas, cela affichera immédiatement 42 dans la console.

  • Si promise1 est résolue, catch est ignoré et on atteint then, qui affiche la valeur dans la console.
    • L'état et la valeur de promise1 seront resolved et "Success!".
    • L'état et la valeur de promise2 seront resolved et "done" ;
  • Si promise1 est rejetée, catch est exécuté, ce qui renvoie une valeur, et donc la chaîne est maintenant resolved, et on atteint then, qui affiche la valeur dans la console.
    • L'état et la valeur de promise1 seront rejected et "Nope!".
    • L'état et la valeur de promise2 seront resolved et "done" ;

finally

Parfois, tu veux exécuter du code une fois qu'une promesse s'est stabilisée, qu'elle soit résolue ou rejetée. Avec promise.finally(onSettled), le callback onSettled ne reçoit rien. Cela renvoie toujours une nouvelle promesse « enchaînée ».

Renvoyer une value depuis finally copie le statut et la valeur de la promesse d'origine, en ignorant la value. Lever une reason dans finally rejette la promesse « enchaînée », en écrasant tout statut et valeur ou raison de la promesse d'origine.

Exemple

Plusieurs de ces méthodes combinées :

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');
  });
  • Dans les cas où randomNumber vaut 0-3 :
    • myPromise sera résolue avec la valeur 2, 4, 6, or 8
    • finalPromise sera résolue avec la valeur 'yay'
    • Deux messages seront affichés dans la console :
      • Sampled data: ...
      • Promise completed
  • Dans les cas où randomNumber vaut 4-5 :
    • myPromise sera rejetée avec la raison 'Sampling did not result in a sample'
    • finalPromise sera rejetée avec la raison Error('Sampling did not result in a sample')
    • Un seul message sera affiché dans la console :
      • Promise completed
      • dans certains environnements, cela produira un message "uncaught rejected promise: Error('Sampling did not result in a sample')" dans la console

Comme montré ci-dessus, reject fonctionne avec une string, et une promesse peut aussi être rejetée avec une Error.

Note

Si l'enchaînement des promesses ou leur utilisation générale n'est pas clair, le tutoriel sur MDN est une bonne ressource à consulter.

Instructions

Dans cet exercice, tu vas fournir un TranslationService qui propose des services de traduction de base aux membres gratuits, et de la traduction avancée aux membres premium avec des garanties de qualité.

L'API

Tu as trouvé une API de traduction de l'espace qui exécute toute request de traduction en un temps raisonnable. Tu veux en tirer parti. Les traducteurs de l'espace sont extrêmement capricieux et détestent la redondance, c'est pourquoi ils fournissent aussi des satellites de stockage API où tu peux fetch les traductions passées sans les déranger.

Récupère une traduction

api.fetch(text) récupère une traduction de text depuis le stockage API et renvoie une promise qui fournit deux valeurs :

  • translation : la traduction elle-même
  • quality : la qualité exprimée sous forme de nombre

Si une traduction est introuvable dans le stockage API, l'API lève une erreur NotAvailable. Les traductions peuvent être ajoutées avec la méthode api.request. Si 'text' n'est pas traduisible, l'API lève une erreur Untranslatable.

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

Demande une traduction

Certaines traductions existent à coup sûr, mais n'ont pas encore été ajoutées au stockage API. C'est la différence entre NotAvailable (absente du stockage, mais qui peut être demandée) et Untranslatable (qui ne peut pas être traduite).

api.request(text, callback) demande qu'une traduction de text soit effectuée et ajoutée dans le stockage API. Une fois l'opération terminée, la fonction callback est appelée.

  • En cas de succès, undefined est passé à callback : cela indique que la traduction a réussi et qu'elle est accessible via la méthode api.fetch.
  • En cas d'échec, une error est passée à callback : cela indique que quelque chose s'est mal passé. L'API spatiale est instable, ce qui signifie qu'elle échoue souvent. Si cela se produit, tu peux relancer api.request.
api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
//        because it was successful.

⚠ Attention ! ⚠

Caution

L'API opère sa magie en téléportant les différents traducteurs dès qu'une request arrive. C'est une action très coûteuse, donc il ne faut pas l'appeler quand une traduction est disponible. Malheureusement, tout le monde ne lit pas le manuel, alors un système a été mis en place pour écarter les clients abusifs.

Si un api.request est appelé pour un text déjà disponible, l'API lève une AbusiveClientError pour cet appel, et pour tous les appels suivants. Veille à ne jamais demander une traduction si quelque chose a déjà été traduit.

1. Récupère une traduction sans tenir compte de la qualité

Le service gratuit ne fournit que les traductions actuellement présentes dans le stockage API.

Implémente une méthode free(text) qui fournit aux membres gratuits les traductions qui existent déjà dans le stockage API. Ignore la qualité et transmets les erreurs levées par l'API.

  • Renvoie la traduction si elle peut être récupérée, quelle que soit sa qualité
  • Transmet toute erreur de l'API de traduction
  • Utilise la méthode api.fetch (api.fetch renvoie une promise)
service.free('jIyaj');
// => Promise<...> resolves "I understand."

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

2. Récupère un lot de traductions, tout ou rien

Implémente une méthode batch([text, text, ...]) pour les membres gratuits qui traduit un tableau de textes avec le service gratuit, en renvoyant toutes les traductions, ou une seule erreur.

  • Résout avec toutes les traductions (dans le même ordre) si elles sont toutes disponibles
  • Rejette avec la première erreur rencontrée
  • Rejette avec une erreur BatchIsEmpty si aucun texte n'est fourni
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. Demande une traduction, en réessayant au maximum 2 fois

Implémente une méthode request(text) pour les utilisateurs premium, qui demande qu'une traduction soit ajoutée au stockage API. La requête doit réessayer automatiquement en cas d'échec. Elle ne doit pas effectuer plus de 3 appels pour la même requête (ne froisse pas les traducteurs de l'espace !!!).

  • Si api.request ne renvoie pas d'erreur, résous avec undefined
  • Si api.request renvoie une erreur, réessaie au maximum deux fois
  • Si tu épuises les tentatives, rejette avec la dernière erreur reçue
service.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API

4. Récupère une traduction, inspecte la qualité, ou demande-la

Implémente une méthode premium(text, quality) pour les utilisateurs premium afin de récupérer une traduction. Si une traduction est NotAvailable, demande la traduction puis récupère-la une fois qu'elle a été ajoutée au stockage API. La méthode ne doit renvoyer la traduction que si elle atteint un certain seuil de quality.

  • Si api.fetch se résout, vérifie la qualité avant de résoudre
  • Si api.fetch échoue, demande la traduction à la place
  • Si api.request échoue, transmets l'erreur
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 traduction correcte de 'arlogh Qoylu'pu'? est How many times has it been heard?.

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
JavaScript Exercism

Prêt à commencer Service de traduction ?

Inscris-toi sur Exercism pour apprendre et maîtriser JavaScript avec 37 concepts159 exercices, et un vrai mentorat humain, le tout gratuitement.