Skip to content

Latest commit

 

History

History
1194 lines (775 loc) · 37.6 KB

js.md

File metadata and controls

1194 lines (775 loc) · 37.6 KB

Javascript

Problems and solutions that are specific to JS.

I. Closure

  1. Make Counter
  2. Make Counter II
  3. Cycle
  4. Memoize
  5. Once

II. JS Functions - Array

  1. Array.prototype.at
  2. Flatten Deeply Nested Array
  3. Array Reduce Transformation
  4. Array.prototype.filter
  5. Array.prototype.map
  6. Array.prototype.last

III. JS Functions - Utility/Lodash

  1. Check if Object Instance of Class
  2. Debounce
  3. Throttle
  4. Deep Equal
  5. Group By
  6. Count By
  7. Convert Object to JSON String
  8. Array Wrapper

IV. JS Functions - Function

  1. Function.prototype.call
  2. Function.prototype.apply

V. JS Functions - Object

  1. Deep Object Filter
  2. Array of Object to Matrix

VI. Functional Programming

  1. Function Composition
  2. Curry

VII. Promise

  1. Sleep
  2. Promise Pool
  3. Promise All
  4. Promise All Settled
  5. Promise Race
  6. Promise Any
  7. Promise Time Limit
  8. Cancellable Promise

VII. Generator

  1. Generate Fibonacci Sequence
  2. Nested Array Generator
  3. Cancellable Function

Make Counter

Description

Leetcode-2620

[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.

Example

const counter = makeCounter();
counter(); // 0
counter(); // 1
counter(); // 2

Implementation

Back to index

Make Counter II

Description

[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.

Example

const counter = makeCounter(1);
counter.getValue(); // 1
counter.increment(2); // 3
counter.decrement(4); // -1
counter.reset(); // 1

Implementation

Back to index

Cycle

Description

[Easy] Implement a function that takes one or more values and returns a function that cycles through those values each time it is called.

Example

const helloFn = cycle("hello");
helloFn(); // "hello"
helloFn(); // "hello"

const onOffFn = cycle("on", "off");
onOffFn(); // "on"
onOffFn(); // "off"
onOffFn(); // "on"

Implementation

Back to index

Memoize

Description

Leetcode-2623

[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.

Example

const sum = (a, b) => a + b;
const memoizedSum = memoize(sum);
memoizedSum(2, 3); // 5
memoizedSum(2, 3); // 5. Cached.

Implementation

Back to index

Once

Description

[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

Example

fn = (a, b) => a + b;
const onceFn = once(fn);
onceFn(2, 3); // 5
onceFn(2, 3); // undefined, fn was not called.

Implementation

Back to index

Array.prototype.at

Description

[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.

Example

const arr = [5, 12, 8, 130, 44];

let index = 2;
arrayAt(arr, index); // 8

index = -2;
arrayAt(arr, index); // 130

Implementation

Back to index

Check if Object Instance of Class

Description

Leetcode-2618

[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.

Example

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

Solution

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.

Implementation

Back to index

Flatten Deeply Nested Array

Description

[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.

Example

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)

Solution

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.

Implementation

Back to index

Debounce

Description

[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.

Implementation

Back to index

Throttle

Description

[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.

Example

Implementation

Back to index

Array Reduce Transformation

Description

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.

Implementation

Back to index

Array.prototype.filter

Description

[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.

Example

const arr = [0, 10, 20, 30];
const fn = function greaterThan10(n) {
  return n > 10;
};
filter(arr, fn); // [20,30]

Implementation

Back to index

Array.prototype.map

Description

[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).

Example

const arr = [1, 2, 3];
const fn = function plusone(n) {
  return n + 1;
};
map(arr, fn); // [2,3,4]

Implementation

Back to index

Array.prototype.last

Description

Leetcode-2619

[Easy] Implement Array.prototype.last method. Given an array and return the last element of the array. If there is no element, return undefined.

Example

arr = [null, {}, 3]; // 3
arr1 = []; // undefined

Implementation

Back to index

Deep Equal

Description

[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.

Example

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

Implementation

Back to index

Convert Object to JSON String

Description

[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().

Example

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}'

Implementation

Back to index

Array Wrapper

Description

[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.

Example

arr1 = [1, 2, 3];
arr2 = [4, 5];
arr1 + arr2; // 15.
String(arr1); // [1, 2, 3]
String(arr2); // [4, 5]

Implementation

Back to index

Function.prototype.call

Description

[Medium] Implement Function.prototype.call() method. The call() method of Function instance calls this function with a given this value and arguments provided individually.

Implementation

Back to index

Function.prototype.apply

Description

[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.

Implementation

Back to index

Deep Object Filter

Description

**[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:

  1. Remove any properties for which the filter function fn returns false.

  2. 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.

Example

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.

Implementation

Back to index

Array of Object to Matrix

Description

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

// 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:
[[], [], [], []];

Solution

The objectToMatrix function handles complex nested data structures by flattening them into dot-notation paths and converting them to matrix format.

Key Features:

  1. Deep Flattening: Recursively traverses nested objects and arrays, creating dot-notation paths (e.g., a.b.c, 0.a.1)

  2. Lexicographical Sorting: Column names are sorted alphabetically as required

  3. Matrix Creation:

    • First row contains sorted column headers
    • Subsequent rows contain values for each object
    • Missing values are filled with empty strings ""
  4. Edge Cases Handled:

    • Empty objects {}
    • Empty arrays []
    • Mixed data types (null, boolean, string, number)
    • Arrays containing objects
    • Deeply nested structures

Algorithm Breakdown:

  1. getAllPaths(): Recursively finds all possible dot-notation paths in the data structure
  2. flattenObject(): Converts nested objects/arrays into flat key-value pairs with dot notation
  3. 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

Implementation

Back to index

Function Composition

Description

[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.

Example

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

Solution

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.

Implementation

Back to index

Group By

Description

[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

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]
}

Implementation

Back to index

Count By

Description

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.

Example

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}

Implementation

Back to index

Curry

Description

[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.

Example

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.

Implementation

Back to index

Sleep

Description

Leetcode-2621

[Easy] Given a positive integer millis, write an asynchronous function that sleeps for millis milliseconds. It can resolve any value.

Example

sleep(100).then(() => {
  console.log("Slept for 100 ms."); // Printed after 100ms.
});

Implementation

Back to index

Promise Pool

Description

[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.

Example

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

Solution

Use Promise.all to execute n functions at a time. When a function resolves, push the next function to the array.

Implementation

Back to index

Promise All

Description

[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.

Example

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

Implementation

Back to index

Promise All Settled

Description

[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:

  1. Already fulfilled, if the iterable passed is empty.
  2. 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.
  3. 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.

Example

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",
  },
];

Implementation

Back to index

Promise Any

Description

[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.

Implementation

Back to index

Promise Race

Description

[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.

Implementation

Back to index

Promise Time Limit

Description

[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".

Implementation

Back to index

Cancellable Promise

Description

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.

Implementation

Generate Fibonacci Sequence

Description

[Easy] Implement a generator function that returns a generator object which yields the fibonacci sequence.

Example

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
// ...

Implementation

Back to index

Nested Array Generator

Description

[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.

Example

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; // 8

Implementation

Back to index

Cancellable Function

Description

Sometimes 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.

Example

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.

Solution

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 using for await.
  • cancel resolves/rejects a dedicated cancelPromise that is created up front. On each iteration, race the generator's yielded promise against cancelPromise with Promise.race, so a call to cancel() 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 with generator.throw(reason), and keep looping.
    • If cancelPromise wins the race, call generator.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).
  • Once cancellation has been delivered once, stop including cancelPromise in future races. Since a settled promise wins every subsequent Promise.race it's part of, continuing to race against an already-rejected cancelPromise would 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 against cancelPromise anymore.

Implementation

Back to index