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:
runAsyncGenerator(generatorFn): This function should accept a generator function (generatorFn).- Execution: It should invoke
generatorFnto get an iterator object. - Promise Handling: When the iterator's
next()method is called and it returns an object with avaluethat is aPromise:runAsyncGeneratormust wait for this promise to resolve.- Once resolved, the resolved value should be passed back into the generator by calling
iterator.next(resolvedValue).
- Non-Promise Yields: If the generator yields a non-promise value,
runAsyncGeneratorshould immediately resume the generator by callingiterator.next(yieldedValue). - Completion: When the generator is done (i.e.,
iterator.next()returns{ done: true, value: finalValue }),runAsyncGeneratorshould return thefinalValueof the generator. - Error Handling: If any yielded promise rejects, or if an error occurs within the generator,
runAsyncGeneratorshould catch this error and propagate it by callingiterator.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
runAsyncGeneratorfunction must return aPromisethat resolves with the generator's return value or rejects with any error encountered. - Input generator functions will only yield
Promiseobjects or primitive values. - No external libraries are allowed for implementing the core
runAsyncGeneratorlogic. - 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
yieldkeyword in a generator function pauses execution and returns a value. Wheniterator.next(value)is called, thevalueis inserted into the generator at the point of theyieldexpression. - Think about how to handle both resolved promises and rejected promises within your
runAsyncGeneratorfunction. - 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.