Exercise Translation Service in JavaScript

  premium(text, minimumQuality) {
    let attempt = 0;

    const attemptFetch = () => {
        return new Promise((resolve, reject) => {
            this.api.fetch(text).then((response) => {
                if (response.quality < minimumQuality) {
                    reject(new QualityThresholdNotMet(text));
                } else {
                    resolve(response.translation);
                }
            }).catch((error) => {
                reject(error);
            });
        });
    };

    const attemptRequestAndFetch = () => {
        return new Promise((resolve, reject) => {
            this.request(text).then(() => {
                attemptFetch().then((response) => {
                  resolve(response);
                }).catch((error) => {
                  reject(new QualityThresholdNotMet(text));
                });
            }).catch((error) => {
              reject(error)
            });
        });
    };

    return attemptFetch().catch((error) => {
        attempt++;
        if (attempt < 2) {
            return attemptRequestAndFetch();
        } else {
            return Promise.reject(error);
        }
    });
}

I am stuck in the last task of this exercise. There is just one error left: Test 18 - Premium service > it recognizes insufficient quality

This is the error that I am getting:

Error: expect(received).rejects.toThrow(expected)

Expected constructor: QualityThresholdNotMet
Received constructor: AbusiveClientError

Received message: "Your client has been rejected because of abusive behaviour.·
naDevvo’ yIghoS!"

      81 |     if (this.values[text] && this.values[text][0]) {
      82 |       mutex.current = true;
    > 83 |       callback(new AbusiveClientError());
         |                ^
      84 |       return;
      85 |     }
      86 |

      at ExternalApi.request (../..<solution>/api.js:83:16)
      at request (../..<solution>/service.js:72:18)
      at attemptResponse (../..<solution>/service.js:71:14)
      at attemptResponse (../..<solution>/service.js:77:13)
      at ExternalApi.callback [as request] (../..<solution>/api.js:83:7)
      at request (../..<solution>/service.js:72:18)
      at attemptResponse (../..<solution>/service.js:71:14)
      at attemptResponse (../..<solution>/service.js:77:13)
      at ExternalApi.callback [as request] (../..<solution>/api.js:83:7)
      at request (../..<solution>/service.js:72:18)
      at attemptResponse (../..<solution>/service.js:71:14)
      at TranslationService.attemptResponse [as request] (../..<solution>/service.js:86:12)
      at request (../..<solution>/service.js:118:18)
      at attemptRequestAndFetch (../..<solution>/service.js:117:16)
      at attemptRequestAndFetch (../..<solution>/service.js:133:20)
      at Object.<anonymous> (../..<solution>/service.spec.js:199:5)

I am just not able to understand how I am getting an abusiveClient error. I am only requesting when it gives error from attemptFetch() function.

It is impossible to tell from your code snippet, what actually happens. Please provide all of your solution, maybe add per-file folding of the code blocks using:

[details="Summary"]
This text will be hidden
[/details]

We can then follow all the pathes in the code up to the point of failure.

Here’s the whole solution:

/// <reference path="./global.d.ts" />
// @ts-check
//
// The lines above enable type checking for this file. Various IDEs interpret
// the @ts-check and reference directives. Together, they give you helpful
// autocompletion when implementing this exercise. You don't need to understand
// them in order to use it.
//
// In your own projects, files, and code, you can play with @ts-check as well.

export class TranslationService {
  /**
   * Creates a new service
   * @param {ExternalApi} api the original api
   */
  constructor(api) {
    this.api = api;
  }

  /**
   * Attempts to retrieve the translation for the given text.
   *
   * - Returns whichever translation can be retrieved, regardless the quality
   * - Forwards any error from the translation api
   *
   * @param {string} text
   * @returns {Promise<string>}
   */
  free(text) {
    return this.api.fetch(text).then(function (response) {
      return response.translation;
    })
    .catch(function (err) {
      throw err;
    });
  }

  /**
   * Batch translates the given texts using the free service.
   *
   * - Resolves all the translations (in the same order), if they all succeed
   * - Rejects with the first error that is encountered
   * - Rejects with a BatchIsEmpty error if no texts are given
   *
   * @param {string[]} texts
   * @returns {Promise<string[]>}
   */
  batch(texts) {
    if(texts.length === 0) {
      return Promise.reject(new BatchIsEmpty());
    }
    try {
      const response = Promise.all(texts.map(str => this.free(str)));
      return response;
    }
    catch(error) {
      return Promise.reject(error);
    }
  }
  /**
   * Requests the service for some text to be translated.
   *
   * Note: the request service is flaky, and it may take up to three times for
   *       it to accept the request.
   *
   * @param {string} text
   * @returns {Promise<void>}
   */
  request(text) {
    const attemptResponse = (attempt = 1) => {
      return new Promise((resolve, reject) => {
        this.api.request(text, function(response) {
          if(response === undefined) {
            resolve(response);
          }
          else if(attempt < 3) {
            attemptResponse(attempt + 1).then(resolve).catch(reject);
          }
          else {
            reject(response);
          }
        })
      })
    }

    return attemptResponse();
  }

  /**
   * Retrieves the translation for the given text
   *
   * - Rejects with an error if the quality can not be met
   * - Requests a translation if the translation is not available, then retries
   *
   * @param {string} text
   * @param {number} minimumQuality
   * @returns {Promise<string>}
   */
  premium(text, minimumQuality) {
    let attempt = 0;

    const attemptFetch = () => {
        return new Promise((resolve, reject) => {
            this.api.fetch(text).then((response) => {
                if (response.quality < minimumQuality) {
                    reject(new QualityThresholdNotMet(text));
                } else {
                    resolve(response.translation);
                }
            }).catch((error) => {
                reject(error);
            });
        });
    };

    const attemptRequestAndFetch = () => {
        return new Promise((resolve, reject) => {
            this.request(text).then(() => {
                attemptFetch().then((response) => {
                  resolve(response);
                }).catch((error) => {
                  reject(new QualityThresholdNotMet(text));
                });
            }).catch((error) => {
              reject(error)
            });
        });
    };

    return attemptFetch().catch((error) => {
        attempt++;
        if (attempt < 2) {
            return attemptRequestAndFetch();
        } else {
            return Promise.reject(error);
        }
    });
}

}


/**
 * This error is used to indicate a translation was found, but its quality does
 * not meet a certain threshold. Do not change the name of this error.
 */
export class QualityThresholdNotMet extends Error {
  /**
   * @param {string} text
   */
  constructor(text) {
    super(
      `
The translation of ${text} does not meet the requested quality threshold.
    `.trim(),
    );

    this.text = text;
  }
}

/**
 * This error is used to indicate the batch service was called without any
 * texts to translate (it was empty). Do not change the name of this error.
 */
export class BatchIsEmpty extends Error {
  constructor() {
    super(
      `
Requested a batch translation, but there are no texts in the batch.
    `.trim(),
    );
  }
}

  1. Do you understand, when AbusiveClientError is produced by the API service?
  2. When you follow the path through your code for the inputs given from the test 18, what has been done before the AbusiveClientError condition happens?
  3. How could you prevent that from happening?

AbusiveClientError is produced when we try to request a text which has already been translated. I am not able to follow the path through the code because I can’t find the algorithm by which the api.js is translating the strings.

You should not need to see api.js in order to figure out what to do, but you can. In the online editor, click the api.js tab:

The instructions say:

Implement a premium user method premium(text, quality) to fetch a translation. If a translation is NotAvailable, request the translation and fetch it after its been added to the API storage. The method should only return the translation if it meets a certain quality threshold.

  • If api.fetch resolves, check the quality before resolving
  • If api.fetch rejects, request the translation instead
  • If api.request rejects, forward the error

Your premium code does:

  • attemptFetch, if any error occurs during it, increase attempt by 1 and if attempt < 2, attemptRequestAndFetch
  • attemptRequestAndFetch will only ever attempt fetch again once, and not retry via attempt, but it will retry in this.request

The issue you’re having is that if this.api.fetch(...) inside attemptFetch returns with a translation, but the quality is bad, the attemptFetch() promise rejects, and you will automatically retry. This will request a translation in attemptRequestAndFetch via this.request which is not allowed, because you already had a translation.


I recommend you rewrite your code to look like the instructions, or rather

get a translation for [text]
- if successful, check quality
  - throw QualityThresholdNotMet if not good enough
  - return translation otherwise

And define the first function get a translation for [text] to be:

function get a translation for (text) {
   fetch(text)
   - if resolved, return it
   - if rejected, request(text)
      - if that resolves, call fetch(text)
}

The upside of doing it this way is that the logic to check the quality is only written (and executed) once, the logic for retrying is encapsulated in request, which you’ve written before. It will never fetch when there is a translation, and finally if request(text) fails or the subsequent fetch(text) after request(text) fails, the entire function get a translation for (text) fails, and thus the entire premium call fails, and the error is forwarded.

It is possible to solve this task using:

  • no new Promise(...)
  • two .then() calls
  • one .catch() call
  • a single throw
  • calls this.api.fetch in two places
  • reuses this.request in one place

You do not need to implement it that way, but you may want to keep thinking about this until you have a solution that works with those constraints.

I have completely rewritten my code and this time it works. Here’s the code:

premium(text, minimumQuality) {
    const getTranslation = () => {
        return this.api.fetch(text).then((response) => {
            if (response.quality < minimumQuality) {
                throw new QualityThresholdNotMet(text);
            }
            return response.translation;
        });
    };

    const attemptRequest = () => {
        return getTranslation().catch((error) => {
            if (error instanceof QualityThresholdNotMet) {
                throw error; 
            }
        
            return this.request(text).then(() => {
                return getTranslation();
            });
        });
    };

    return attemptRequest().then((translation) => {
        return translation; 
    }).catch((error) => {
        throw error; 
    });
}

Thank you @mk-mxp and @SleeplessByte for helping me in solving this exercise.

1 Like