Problems and solutions that are specific to JS.
I. Closure
II. JS Functions - Array
- Array.prototype.at
- Flatten Deeply Nested Array
- Array Reduce Transformation
- Array.prototype.filter
- Array.prototype.map
- Array.prototype.last
III. JS Functions - Utility/Lodash
- Check if Object Instance of Class
- Debounce
- Throttle
- Deep Equal
- Group By
- Count By
- Convert Object to JSON String
- Array Wrapper
IV. JS Functions - Function
V. JS Functions - Object
VI. Functional Programming
VII. Promise
- Sleep
- Promise Pool
- Promise All
- Promise All Settled
- Promise Race
- Promise Any
- Promise Time Limit
- Cancellable Promise
VII. Generator
[Easy] Implement a function that accepts an optional integer number (default to 0) and returns a function. When the returned function is called initially, it returns the initial value. Then, every time it's called, the number is incremented by 1.
const counter = makeCounter();
counter(); // 0
counter(); // 1
counter(); // 2[Medium] Implement a function that accepts an optional integer number (default to 0) and returns a counter object with the methods to retrieve and manipulate the value. By manipulating value, it should be able to increment, decrement and reset the value.
const counter = makeCounter(1);
counter.getValue(); // 1
counter.increment(2); // 3
counter.decrement(4); // -1
counter.reset(); // 1[Easy] Implement a function that takes one or more values and returns a function that cycles through those values each time it is called.
const helloFn = cycle("hello");
helloFn(); // "hello"
helloFn(); // "hello"
const onOffFn = cycle("on", "off");
onOffFn(); // "on"
onOffFn(); // "off"
onOffFn(); // "on"[Medium] Given a function fn, return a memoized version of that function.
A memoized function is a function that will never be called twice with the same inputs. Instead it will return a cached value.
You can assume there are 3 possible input functions: sum, fib, and factorial.
sum accepts two integers a and b and returns a + b. Assume that if a value has already been cached for the arguments (b, a) where a != b, it cannot be used for the arguments (a, b). For example, if the arguments are (3, 2) and (2, 3), two separate calls should be made.
fib accepts a single integer n and returns 1 if n <= 1 or fib(n - 1) + fib(n - 2) otherwise.
factorial accepts a single integer n and returns 1 if n <= 1 or factorial(n - 1) * n otherwise.
const sum = (a, b) => a + b;
const memoizedSum = memoize(sum);
memoizedSum(2, 3); // 5
memoizedSum(2, 3); // 5. Cached.[Easy] Given a function fn, return a new function that is identical to the original function except that it ensures fn is called at most once. The first time it gets called, it returns the exact same result as fn. Event subsequent time it is called, it should return undefined
fn = (a, b) => a + b;
const onceFn = once(fn);
onceFn(2, 3); // 5
onceFn(2, 3); // undefined, fn was not called.[Easy] Implement Array.prototype.at method. The arrayAt() function takes an array and an integer value and returns the item at that index, allowing for positive and negative integers. Negative integers count back from the last item in the array.
const arr = [5, 12, 8, 130, 44];
let index = 2;
arrayAt(arr, index); // 8
index = -2;
arrayAt(arr, index); // 130[Medium] Write a function that checks if a given value is an instance of a given class or superclass. For this problem, an object is considered an instance of a given class if that object has access to that class's methods.
There are no constraints on the data types that can be passed to the function. For example, the value or the class could be undefined.
func = () => checkIfInstanceOf(new Date(), Date); // true. The object returned by the Date constructor is, by definition, an instance of Date.
func = () => checkIfInstanceOf(Date, Date); // false. A date constructor cannot logically be an instance of itself.
func = () => checkIfInstanceOf(5, Number); // true. 5 is a Number. Note that the "instanceof" keyword would return false. However, it is still considered an instance of Number because it accesses the Number methods. For example "toFixed()".
func = () => {
class Animal {}
class Dog extends Animal {}
return checkIfInstanceOf(new Dog(), Animal); // true. Dog is a subclass of Animal. Therefore, a Dog object is an instance of both Dog and Animal.
};Every object has a __proto__ property that points to class's prototype. Traverse up the chain until we either: find a match with the class's prototype or reach the end of the chain.
If the given classFunction is null or undefined, return false.
[Medium] Given a multi-dimensional array arr and a depth n, return a flattened version of that array.
A multi-dimensional array is a recursive data structure that contains integers or other multi-dimensional arrays.
A flattened array is a version of that array with some or all of the sub-arrays removed and replaced with the actual elements in that sub-array. This flattening operation should only be done if the current depth of nesting is less than n. The depth of the elements in the first array are considered to be 0.
arr = [1,2,3,[4,5,6],[7,8,[9,10,11],12],[13,14,15]]
n = 0
Output: [1,2,3,[4,5,6],[7,8,[9,10,11],12],[13,14,15]](Flatten 0-depth array, i.e. no sub-array is flattened)
n = 1
Output: [1,2,3,4,5,6,7,8,[9,10,11],12,13,14,15] (Flatten one-level depth array)
n = 2
Output: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] (Flatten two-level depth array)
Use recursion to flatten the array. If the current element is an array and the depth is less than n, flatten the array and concatenate the result. Otherwise, push the element to the result.
[Medium] Given a function fn and a time in milliseconds t, return a debounced version of that function.
A debounced function is a function whose execution is delayed by t milliseconds and whose execution is cancelled if it is called again within that window of time. The debounced function should also receive the passed parameters.
For example, let's say t = 50ms, and the function was called at 30ms, 60ms, and 100ms.
The first 2 function calls would be cancelled, and the 3rd function call would be executed at 150ms.
If instead t = 35ms, The 1st call would be cancelled, the 2nd would be executed at 95ms, and the 3rd would be executed at 135ms.
[Medium] Given a function fn and a time in milliseconds t, return a throttled version of that function.
A throttled function is a function which is called immediately and within the next t milliseconds it cann't be called again until time is up. When the function is called again, it should be called with the latest arguments.
Given an array, a reducer function fn, and an initial value init, return the final result obtained by executing the fn function on each element of the array, sequentially, passing in the return value from the calculation on the preceding element.
This result is achieved through the following operations: val = fn(init, arr[0]), val = fn(val, arr[1]), val = fn(val, arr[2]), ... until every element in the array has been processed. The ultimate value of val is then returned.
If the length of the array is 0, the function should return init.
[Easy] Implement Array.prototype.filter. Given an array arr and a filtering function fn, return a filtered array filteredArr. The filteredArr should only contain the elements from the arr for which the expression fn(arr[i], i) evaluates to a truthy value. A truthy value is a value where Boolean(value) returns true.
const arr = [0, 10, 20, 30];
const fn = function greaterThan10(n) {
return n > 10;
};
filter(arr, fn); // [20,30][Easy] Implement Array.prototype.map method. Given an array arr and a function fn, return a new array with a transformation applied to each element.
The returned array should be created such that returnedArray[i] = fn(arr[i], i).
const arr = [1, 2, 3];
const fn = function plusone(n) {
return n + 1;
};
map(arr, fn); // [2,3,4][Easy] Implement Array.prototype.last method. Given an array and return the last element of the array. If there is no element, return undefined.
arr = [null, {}, 3]; // 3
arr1 = []; // undefined[Medium] Given two objects o1 and o2, check if they are deeply equal.
For two objects to be deeply equal, they must contain the same keys, and the associated values must also be deeply equal. Two objects are also considered deeply equal if they pass the === equality check.
You may assume both objects are the output of JSON.parse. In other words, they are valid JSON.
o1 = {"x":1,"y":2}, o2 = {"x":1,"y":2} -> true
o1 = {"y":2,"x":1}, o2 = {"x":1,"y":2} -> true
o1 = {"x":null,"L":[1,2,3]}, o2 = {"x":null,"L":["1","2","3"]} -> false
o1 = true, o2 = false -> false
[Medium] Given an object, return a valid JSON string of that object. You may assume the object only includes strings, integers, arrays, objects, booleans, and null. The returned string should not include extra spaces. The order of keys should be the same as the order returned by Object.keys().
const obj = { key: "value", number: 123, boolean: true, null: null };
const jsonStr = jsonStringify(obj);
console.log(jsonStr); // Output: '{"key":"value","number":123,"boolean":true,"null":null}'[Easy] Create a class ArrayWrapper that accepts an array of integers in its constructor. This class should have two features: 1. When two instances of this class are added together with the + operator, the resulting value is the sum of all the elements in both arrays. 2. When the String() function is called on the instance, it will return a comma separated string surrounded by brackets.
arr1 = [1, 2, 3];
arr2 = [4, 5];
arr1 + arr2; // 15.
String(arr1); // [1, 2, 3]
String(arr2); // [4, 5][Medium] Implement Function.prototype.call() method. The call() method of Function instance calls this function with a given this value and arguments provided individually.
[Medium] Implement Function.prototype.apply() method. The apply() method of Function instance calls this function with a given this value and arguments provided as an array or an array-like object.
**[Medium]**Given an object obj and a function fn, return a filtered object called filteredObject.
The function deepObjectFilter should perform a deep filtering operation on the object obj. The deep filtering process should:
-
Remove any properties for which the filter function fn returns false.
-
Recursively remove any empty objects or arrays that remain after their keys or elements have been filtered out.
If the deep filtering operation results in an object or array that is empty (i.e., it has no remaining properties or elements), deepObjectFilter should return undefined, indicating that there is no valid data left in filteredObject.
obj = [-5, -4, -3, -2, -1, 0, 1];
fn = (x) => x > 0;
Returns: [1]; // The value that is not greater than 0 is filtered out.
obj = {"a": 1, "b": "2", "c": 3, "d": "4", "e": 5, "f": 6, "g": {"a": 1}}
fn = x => typeof x === 'string'
Returns: {"b": "2", "d": "4"}. // The value that is not a string is filtered out. The empty object is also removed.
obj = [-1, [-1, -1, 5, -1, 10], -1, [-1], [-5]],
fn = (x) => x > 0
Returns: [[5, 10]] // The value that is not greater than 0 is removed. The empty array is also removed.
obj = [[[[5]]]],
fn = (x) => Array.isArray(x)
Returns: undefined // Recursively run deepFilterObject until it finds 5, which is not an array.Write a function that converts an array of objects arr into a matrix m.
arr is an array consisting of objects (or arrays). Each item in the array may contain deeply nested sub-arrays and sub-objects. It may also contain numbers, strings, booleans, and null values.
The first row of the matrix m should be the column names.
-
If there is no nesting, the column names are the unique keys in the objects.
-
If nesting exists, the column names should be the corresponding paths in the object, separated by a dot ".".
Each remaining row corresponds to one object in arr. Each value in the matrix corresponds to a value in the object. If a given object does not have a value for a given column, the matrix should contain an empty string "".
The columns in the matrix should be sorted in lexicographical (ascending) order.
// Example 1:
arr = [
{ b: 1, a: 2 },
{ b: 3, a: 4 },
];
// Output:
[
["a", "b"],
[2, 1],
[4, 3],
];
// Example 2:
arr = [{ a: 1, b: 2 }, { b: 3, c: 4, d: 5 }, {}];
// Output:
[
["a", "b", "c", "d"],
[1, 2, "", ""],
["", 3, 4, 5],
["", "", "", ""],
];
// Example 3:
arr = [{ a: { b: 1, c: 2 } }, { a: { b: 3, d: 4 } }];
// Output:
[
["a.b", "a.c", "a.d"],
[1, 2, ""],
[3, "", 4],
];
// Example 4:
arr = [[{ a: null }, { b: true }, { c: "x" }]];
// Output:
[
["0.a", "0.b", "0.c"],
[null, "", ""],
["", true, ""],
["", "", "x"],
];
// Example 5:
// Empty object without any key or value.
arr = [{}, {}, {}];
// Output:
[[], [], [], []];The objectToMatrix function handles complex nested data structures by flattening them into dot-notation paths and converting them to matrix format.
Key Features:
-
Deep Flattening: Recursively traverses nested objects and arrays, creating dot-notation paths (e.g.,
a.b.c,0.a.1) -
Lexicographical Sorting: Column names are sorted alphabetically as required
-
Matrix Creation:
- First row contains sorted column headers
- Subsequent rows contain values for each object
- Missing values are filled with empty strings
""
-
Edge Cases Handled:
- Empty objects
{} - Empty arrays
[] - Mixed data types (null, boolean, string, number)
- Arrays containing objects
- Deeply nested structures
- Empty objects
Algorithm Breakdown:
getAllPaths(): Recursively finds all possible dot-notation paths in the data structureflattenObject(): Converts nested objects/arrays into flat key-value pairs with dot notation- Main logic: Collects all paths, sorts them, and builds the matrix row by row
Time Complexity: O(n × m × d) where n = number of objects, m = average paths per object, d = average nesting depth
Space Complexity: O(p) where p = total number of unique paths
[Easy] Given an array of functions [f1, f2, f3, ..., fn], return a new function fn that is the function composition of the array of functions.
The function composition of [f(x), g(x), h(x)] is fn(x) = f(g(h(x))).
The function composition of an empty list of functions is the identity function f(x) = x.
You may assume each function in the array accepts one integer as input and returns one integer as output.
compose(x => x + 1, x => x * x, x => 2 * x) x = 4 -> 65 Start from the rightmost function: 2 x 4 = 8 8 x 8 = 64 64 + 1 = 65
Use reduceRight to iterate the array from right to left. The initial value is the identity function x => x. For each function, call it with the previous result.
[Medium] Implement a function that takes an array arr and a function fn as arguments and returns a grouped version of the array.
A grouped array is an object where each key is the output of fn(arr[i]) and each value is an array containing all items in the original array which generate that key.
The provided callback fn will accept an item in the array and return a string key.
The order of each value list should be the order the items appear in the array. Any order of keys is acceptable.
Example 1:
arr = [
{"id":"1"},
{"id":"1"},
{"id":"2"}
]
fn = (item) => item.id;
->
{
"1": [{"id": "1"}, {"id": "1"}],
"2": [{"id": "2"}]
}Example 2:
arr = [
[1,2,3],
[1,3,5],
[1,5,9]
]
fn = (list) => list[0];
->
{
"1": [[1,2,3],[1,3,5],[1,5,9]]
}Example 3:
arr = [1,2,3,4,5,6,7,8,9,10]
fn = (n) => String(n > 5);
->
{
"true": [6,7,8,9,10],
"false": [1,2,3,4,5]
}Implement a function that takes an array arr and a function fn as arguments and returns an object composed of keys generated from the results of running each element of the array through the function. The corresponding value of each key is the number of times the key was returned by the function.
arr = [6.1, 4.2, 6.3]
fn = n => Math.floor(n)
countBy(arr, fn) -> {
'4': 1,
'6': 2,
};Explanation: Pass each element in array to fn:
fn(6.1) = 6
fn(4.2) = 4
fn(6.3) = 6
The result of 6 appears twice and 4 appears once. So the returned object key is the result and the value is the number of times the key was returned by the function: {'4': 1, '6': 2}
[Medium] Given a function fn, return a curried version of that function.
A curried function is a function that accepts fewer or an equal number of parameters as the original function and returns either another curried function or the same value the original function would have returned.
sum = (a, b, c) => a + b + c;
curreidSum = curry(sum);
curriedSum(1)(2)(3); // 6
curriedSum(1, 2)(3); // 6
curriedSum(1)(2, 3); // 6
curriedSum(1, 2, 3); // 6
// All the above functions return same result.[Easy] Given a positive integer millis, write an asynchronous function that sleeps for millis milliseconds. It can resolve any value.
sleep(100).then(() => {
console.log("Slept for 100 ms."); // Printed after 100ms.
});[Difficult] Given an array of asynchronous functions functions and a pool limit n, return an asynchronous function promisePool. It should return a promise that resolves when all the input functions resolve.
Pool limit is defined as the maximum number promises that can be pending at once. promisePool should begin execution of as many functions as possible and continue executing new functions when old promises resolve. promisePool should execute functions[i] then functions[i + 1] then functions[i + 2], etc. When the last promise resolves, promisePool should also resolve.
For example, if n = 1, promisePool will execute one function at a time in series. However, if n = 2, it first executes two functions. When either of the two functions resolve, a 3rd function should be executed (if available), and so on until there are no functions left to execute.
Assume all functions never reject. It is acceptable for promisePool to return a promise that resolves any value.
fn1 = () => new Promise((res) => setTimeout(res, 300));
fn2 = () => new Promise((res) => setTimeout(res, 400));
fn3 = () => new Promise((res) => setTimeout(res, 200));promisePool([fn1, fn2, fn3], 1) -> 900ms
Each function is executed asynchronously one by one. The total time is 900ms(300 + 400 + 200).
promisePool([fn1, fn2, fn3], 2) -> 500ms
Two functions(fn1 and fn2) can be running at the same time.
After 300ms, fn1 is resolved and fn3 starts running.
After 100ms, fn2 is resolved.
After another 100ms, fn3 is resolved.
The total time is 500ms(300 + 100 + 100).
promisePool([fn1, fn2, fn3], 3) -> 400ms
All three functions can be running at the same time.
So the total time is the time of the longest function, which is fn2
Use Promise.all to execute n functions at a time. When a function resolves, push the next function to the array.
[Medium] Given an array of promises promises, return a new promise. This new promise should resolve when all of the input promises resolve, with a array of the resolved values. If any of the input promises reject, the new promise should reject with the reason of the first rejection.
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 3000, "foo");
});
const promise4 = Promise.reject(new Error("error"));
promiseAll([promise1, promise2, promise3]);
// expected resolved output: Array [3, 42, "foo"]
promiseAll([promise1, promise2, promise3, promise4]);
// expected rejected output: Error: error[Medium] Given an array of promises promises, return a new promise. This returned promise fulfills when all of the input's promises settle (including when an empty iterable is passed), with an array of objects that describe the outcome of each promise.
The returned Promise is a Promise that is:
- Already fulfilled, if the iterable passed is empty.
- Asynchronously fulfilled, when all promises in the given iterable have settled (either fulfilled or rejected). The fulfillment value is an array of objects, each describing the outcome of one promise in the iterable, in the order of the promises passed, regardless of completion order.
- Each outcome object has the following properties:
-
status: A string, either "fulfilled" or "rejected", indicating the eventual state of the promise.
-
value: Only present if status is "fulfilled". The value that the promise was fulfilled with.
-
reason: Only present if status is "rejected". The reason that the promise was rejected with.
If the iterable passed is non-empty but contains no pending promises, the returned promise is still asynchronously (instead of synchronously) fulfilled.
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 3000, "foo");
});
const promise4 = Promise.reject(new Error("error"));
promiseAllSettled([promise1, promise2, promise3, promise4]);
// It should return:
[
{
status: "fulfilled",
value: 3,
},
{
status: "fulfilled",
value: 42,
},
{
status: "fulfilled",
value: "foo",
},
{
status: "rejected",
reason: "Error",
},
];[Medium] Given an array of promises promises, return a new promise. The returned promise fulfills when any of the iinput promises fulfills, with the first fulfilled value. It rejects when all of the input promises reject (including when an empty iterable is passed) with an AggregateError containing an array of rejection reasons.
[Medium] Given an array of promises promises, return a new promise. This returned promise settles with the eventual state of the first promise that settles.
[Medium] Given an asynchronous function fn and a time limit t in milliseconds, return a time limited version of the input function. The time limited function should follow the rules: 1. If the fn completes within the time limit of t milliseconds, the time limited function should resolve with the result. 2. If the execution of the fn exceeds the time limit, the time limited function should reject with the string "Time Limit Exceeded".
Write a function to accept an argument of asynchronous function fn and return a cancel function. When cancel function is called and the asynchronous function fn is not completed, the fn is cancelled.
[Easy] Implement a generator function that returns a generator object which yields the fibonacci sequence.
const gen = fibGenerator();
gen.next().value; // 0
gen.next().value; // 1
gen.next().value; // 1
gen.next().value; // 2
gen.next().value; // 3
gen.next().value; // 5
gen.next().value; // 8
gen.next().value; // 13
// ...[Medium] Given a multi-dimensional array of integers, return a generator object which yields integers in the same order as inorder traversal.
A multi-dimensional array is a recursive data structure that contains both integers and other multi-dimensional arrays.
Inorder traversal iterates over each array from left to right, yielding any integers it encounters or applying inorder traversal to any arrays it encounters.
const arr = [[[6]], [1, 3], [[4, 5], 2, [7, [9, 8]]]];
const generator = nestedArrayInorderGenerator(arr);
generator.next().value; // 6
generator.next().value; // 1
generator.next().value; // 3
generator.next().value; // 4
generator.next().value; // 5
generator.next().value; // 2
generator.next().value; // 7
generator.next().value; // 9
generator.next().value; // 8Sometimes you have a long running task, and you may wish to cancel it before it completes. To help with this goal, write a function cancellable that accepts a generator object and returns an array of two values: a cancel function and a promise.
You may assume the generator function will only yield promises. It is your function's responsibility to pass the values resolved by the promise back to the generator. If the promise rejects, your function should throw that error back to the generator.
If the cancel callback is called before the generator is done, your function should throw an error back to the generator. That error should be the string "Cancelled" (Not an Error object). If the error was caught, the returned promise should resolve with the next value that was yielded or returned. Otherwise, the promise should reject with the thrown error. No more code should be executed.
When the generator is done, the promise your function returned should resolve the value the generator returned. If, however, the generator throws an error, the returned promise should reject with the error.
An example of how your code would be used:
function* tasks() {
const val = yield new Promise((resolve) => resolve(2 + 2));
yield new Promise((resolve) => setTimeout(resolve, 100));
return val + 1;
}
const [cancel, promise] = cancellable(tasks());
setTimeout(cancel, 50);
promise.catch(console.log); // Logs "Cancelled" at 50ms (because the promise resolves at 100ms and cancel happens before that.)Examaple 1
function* generatorFunction() {
return 42;
}
const cancelledAt = 100;
const [cancel, promise] = cancellable(generatorFunction());
setTimeout(cancel, cancelledAt);
promise.then(console.log); // resolves 42 at t=0ms.Explanation: The generator immediately yields 42 and finishes. Because of that, the returned promise immediately resolves 42. Note that cancelling a finished generator does nothing.
Example 2
generatorFunction = function* () {
const msg = yield new Promise((res) => res("Hello"));
throw `Error: ${msg}`;
};
cancelledAt = null;Output: {"rejected": "Error: Hello"}
Explanation: A promise is yielded. The function handles this by waiting for it to resolve and then passes the resolved value back to the generator. Then an error is thrown which has the effect of causing the promise to reject with the same thrown error.
Example 3
function* generatorFunction() {
yield new Promise((res) => setTimeout(res, 200));
return "Success";
}
canceledAt = 100;Output: {"rejected": "Cancelled"}
Explanation: While the function is waiting for the yielded promise to resolve, cancel() is called. This causes an error message to be sent back to the generator. Since this error is uncaught, the returned promise rejected with this error.
Example 4
generatorFunction = function* () {
let result = 0;
yield new Promise((res) => setTimeout(res, 100));
result += yield new Promise((res) => res(1));
yield new Promise((res) => setTimeout(res, 100));
result += yield new Promise((res) => res(1));
return result;
};
cancelledAt = null;Output: {"resolved": 2}
Explanation: 4 promises are yielded. Two of those promises have their values added to the result. After 200ms, the generator finishes with a value of 2, and that value is resolved by the returned promise.
Example 5
generatorFunction = function* () {
let result = 0;
try {
yield new Promise((res) => setTimeout(res, 100));
result += yield new Promise((res) => res(1));
yield new Promise((res) => setTimeout(res, 100));
result += yield new Promise((res) => res(1));
} catch (e) {
return result;
}
return result;
};
cancelledAt = 150;Output: {"resolved": 1}
Explanation: The first two yielded promises resolve and cause the result to increment. However, at t=150ms, the generator is cancelled. The error sent to the generator is caught and the result is returned and finally resolved by the returned promise.
Example 6
generatorFunction = function* () {
try {
yield new Promise((resolve, reject) => reject("Promise Rejected"));
} catch (e) {
let a = yield new Promise((resolve) => resolve(2));
let b = yield new Promise((resolve) => resolve(2));
return a + b;
}
};
cancelledAt = null;Output: {"resolved": 4}
Explanation: The first yielded promise immediately rejects. This error is caught. Because the generator hasn't been cancelled, execution continues as usual. It ends up resolving 2 + 2 = 4.
The generator can't be driven with a simple for...of loop because cancellation needs to interrupt whichever promise is currently being awaited, not just run to completion.
- Manually drive the generator by calling
generator.next(value)/generator.throw(error)in a loop, instead of usingfor await. cancelresolves/rejects a dedicatedcancelPromisethat is created up front. On each iteration, race the generator's yielded promise againstcancelPromisewithPromise.race, so a call tocancel()can win immediately even while the yielded promise is still pending.- If the yielded promise settles first, feed its value back with
generator.next(value), or its rejection back withgenerator.throw(reason), and keep looping. - If
cancelPromisewins the race, callgenerator.throw("Cancelled"). If the generator doesn't catch it, the throw propagates out and the outer promise rejects with"Cancelled". If the generator catches it, execution continues normally (e.g. returning a value, or yielding further promises).
- If the yielded promise settles first, feed its value back with
- Once cancellation has been delivered once, stop including
cancelPromisein future races. Since a settled promise wins every subsequentPromise.raceit's part of, continuing to race against an already-rejectedcancelPromisewould incorrectly treat every later yield as a fresh cancellation, even after the generator has caught the first one and chosen to keep running. - When the loop sees
done: true, resolve the outer promise with the generator's return value. Cancelling an already-finished generator is a no-op because nothing is left racing againstcancelPromiseanymore.