Hone logo
Hone
Problems

Implementing Generator-Based Asynchronous Operations in JavaScript

JavaScript's asynchronous nature can sometimes lead to complex callback structures or chaining. Generator functions offer an elegant way to manage asynchronous code by allowing you to pause execution and resume it later, making asynchronous code appear more synchronous and readable. This challenge will guide you through building a simple asynchronous execution engine using JavaScript generators.

Problem Description

Your task is to implement a function, let's call it runAsyncGenerator, that takes a generator function as input and executes its asynchronous operations sequentially. The generator function will yield promises, and runAsyncGenerator should wait for each promise to resolve before resuming the generator and passing the resolved value back into it.

Key Requirements:

  1. runAsyncGenerator(generatorFn): This function should accept a generator function (generatorFn).
  2. Execution: It should invoke generatorFn to get an iterator object.
  3. Promise Handling: When the iterator's next() method is called and it returns an object with a value that is a Promise:
    • runAsyncGenerator must wait for this promise to resolve.
    • Once resolved, the resolved value should be passed back into the generator by calling iterator.next(resolvedValue).
  4. Non-Promise Yields: If the generator yields a non-promise value, runAsyncGenerator should immediately resume the generator by calling iterator.next(yieldedValue).
  5. Completion: When the generator is done (i.e., iterator.next() returns { done: true, value: finalValue }), runAsyncGenerator should return the finalValue of the generator.
  6. Error Handling: If any yielded promise rejects, or if an error occurs within the generator, runAsyncGenerator should catch this error and propagate it by calling iterator.throw(error) and then stop execution.

Examples

Example 1: Sequential Promises

function* asyncTaskSequence() {
  console.log("Starting task 1...");
  const result1 = yield new Promise(resolve => setTimeout(() => resolve("Result 1"), 100));
  console.log("Task 1 completed with:", result1);

  console.log("Starting task 2...");
  const result2 = yield new Promise(resolve => setTimeout(() => resolve("Result 2"), 50));
  console.log("Task 2 completed with:", result2);

  return "All tasks finished!";
}

// Assuming runAsyncGenerator is implemented
runAsyncGenerator(asyncTaskSequence)
  .then(finalResult => console.log("Final outcome:", finalResult))
  .catch(error => console.error("An error occurred:", error));

// Expected Console Output (order of "Starting" and "completed" might vary slightly due to logging timing):
// Starting task 1...
// Task 1 completed with: Result 1
// Starting task 2...
// Task 2 completed with: Result 2
// Final outcome: All tasks finished!

Example 2: Handling Non-Promise Yields and Immediate Resolution

function* mixedYields() {
  console.log("Yielding a string directly.");
  const immediateValue = yield "Hello";
  console.log("Received immediate value:", immediateValue);

  console.log("Yielding a promise.");
  const promiseResult = yield Promise.resolve("Async value");
  console.log("Received promise result:", promiseResult);

  return "Done with mixed yields.";
}

// Assuming runAsyncGenerator is implemented
runAsyncGenerator(mixedYields)
  .then(finalResult => console.log("Final outcome:", finalResult))
  .catch(error => console.error("An error occurred:", error));

// Expected Console Output:
// Yielding a string directly.
// Received immediate value: Hello
// Yielding a promise.
// Received promise result: Async value
// Final outcome: Done with mixed yields.

Example 3: Error Handling

function* errorProneGenerator() {
  console.log("First step...");
  yield new Promise(resolve => setTimeout(() => resolve("Success 1"), 50));
  console.log("Second step...");
  yield Promise.reject(new Error("Something went wrong!"));
  console.log("This won't be reached.");
}

// Assuming runAsyncGenerator is implemented
runAsyncGenerator(errorProneGenerator)
  .then(finalResult => console.log("Final outcome:", finalResult))
  .catch(error => console.error("Caught error:", error.message));

// Expected Console Output:
// First step...
// Second step...
// Caught error: Something went wrong!

Constraints

  • The runAsyncGenerator function must return a Promise that resolves with the generator's return value or rejects with any error encountered.
  • Input generator functions will only yield Promise objects or primitive values.
  • No external libraries are allowed for implementing the core runAsyncGenerator logic.
  • The solution should be efficient and not introduce unnecessary delays or memory overhead.

Notes

  • Consider how you will manage the state of the generator and the current iteration.
  • The yield keyword in a generator function pauses execution and returns a value. When iterator.next(value) is called, the value is inserted into the generator at the point of the yield expression.
  • Think about how to handle both resolved promises and rejected promises within your runAsyncGenerator function.
  • The Promise.resolve() static method can be useful for wrapping non-promise values into promises, though it's not strictly necessary if you explicitly check the type.
Loading editor...
javascript