LeetCode Problem Workspace
Design Cancellable Function
Master designing cancellable functions with generators, handling promise flow, cancellation, and error propagation effectively in JavaScript.
0
Topics
1
Code langs
0
Related
Practice Focus
Hard · Design Cancellable Function core interview pattern
Answer-first summary
Master designing cancellable functions with generators, handling promise flow, cancellation, and error propagation effectively in JavaScript.
Ace coding interviews with Interview AiBoxInterview AiBox guidance for Design Cancellable Function core interview pattern
This problem focuses on creating a cancellable function that wraps a generator yielding promises. You must ensure two-way communication between the generator and the promises, passing resolved values back or throwing errors when promises reject. Properly handling the cancel callback is essential, as it should throw a "Cancelled" string into the generator, ensuring no further code executes and the returned promise resolves or rejects correctly.
Problem Statement
You are asked to implement a function called cancellable that accepts a generator object which yields promises. Your function should return an array containing two elements: a cancel function and a promise representing the generator's final result. The promise should resolve or reject according to the generator's flow, and each yielded promise's resolved value should be passed back into the generator. If a yielded promise rejects, that error should be thrown back into the generator.
If the cancel function is invoked before the generator completes, the generator should immediately receive a thrown string "Cancelled". If the generator catches this error, the returned promise continues with the next yielded value or returned value. If the generator does not catch it, the returned promise must reject with "Cancelled". No further code in the generator should run after cancellation. Assume all generators yield only promises and are properly structured.
Examples
Example 1
Input: See original problem statement.
Output: See original problem statement.
function* tasks() { const val = yield new Promise(resolve => resolve(2 + 2)); yield new Promise(resolve => setTimeout(resolve, 100)); return val + 1; // calculation shouldn't be done. } const [cancel, promise] = cancellable(tasks()); setTimeout(cancel, 50); promise.catch(console.log); // logs "Cancelled" at t=50ms
Example 2
Input: generatorFunction = function*() { return 42; } cancelledAt = 100
Output: {"resolved": 42}
const generator = generatorFunction(); const [cancel, promise] = cancellable(generator); setTimeout(cancel, 100); promise.then(console.log); // resolves 42 at t=0ms
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 3
Input: generatorFunction = function*() { const msg = yield new Promise(res => res("Hello")); throw Error: ${msg}; } cancelledAt = null
Output: {"rejected": "Error: Hello"}
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.
Constraints
- cancelledAt == null or 0 <= cancelledAt <= 1000
- generatorFunction returns a generator object
Solution Approach
Wrap Generator with Async Loop
Create an async function that iteratively calls next or throw on the generator. Await each yielded promise and send its resolved value back. Catch promise rejections and throw them into the generator to preserve error propagation.
Implement Cancel Function
Maintain a flag indicating cancellation. The cancel function should throw the string "Cancelled" into the generator if called before completion. Ensure the generator's catch blocks handle it correctly to resolve the returned promise, otherwise reject immediately.
Connect Promises and Generator Flow
Ensure the returned promise resolves when the generator finishes normally or rejects when an uncaught error occurs. Properly propagate both cancellation and generator-thrown errors to the promise, while avoiding executing further code in the generator after cancellation.
Complexity Analysis
| Metric | Value |
|---|---|
| Time | Depends on the final approach |
| Space | Depends on the final approach |
Time and space complexity depend on the number of yielded promises and the overhead of awaiting each. Each promise is handled sequentially, and memory is used to store state and intermediate values.
What Interviewers Usually Probe
- Expect candidates to recognize the two-way communication between generator and async evaluation.
- Watch for proper handling of promise rejections thrown back into the generator.
- Ensure cancellation stops generator execution and resolves or rejects the promise appropriately.
Common Pitfalls or Variants
Common pitfalls
- Failing to propagate promise rejections back into the generator correctly.
- Allowing generator code to continue running after cancellation is invoked.
- Returning a resolved promise instead of rejecting when cancellation is uncaught.
Follow-up variants
- Support multiple simultaneous cancellable generators with independent cancel functions.
- Modify to allow cancellation with custom error messages instead of "Cancelled".
- Extend to handle synchronous yields alongside promises in the generator.
FAQ
What does the cancel function do in the Design Cancellable Function problem?
It throws the string "Cancelled" into the generator. If uncaught, the returned promise rejects immediately and no further code executes.
Can the generator yield non-promise values?
No, the generator is assumed to yield only promises. Handling non-promises is not required for this problem.
What happens if a yielded promise rejects?
The rejection is thrown into the generator, allowing it to handle the error. If uncaught, the returned promise rejects with that error.
Does cancelling a completed generator affect the promise?
No, if the generator has already finished, calling cancel does nothing and the returned promise remains resolved or rejected as usual.
Why is two-way communication important in this problem?
It ensures that each yielded promise's result is correctly passed back and that errors, including cancellation, are propagated properly to maintain correct generator behavior.
Solution
Solution 1
#### TypeScript
function cancellable<T>(generator: Generator<Promise<any>, T, unknown>): [() => void, Promise<T>] {
let cancel: () => void = () => {};
const cancelPromise = new Promise((resolve, reject) => {
cancel = () => reject('Cancelled');
});
cancelPromise.catch(() => {});
const promise = (async () => {
let next = generator.next();
while (!next.done) {
try {
next = generator.next(await Promise.race([next.value, cancelPromise]));
} catch (e) {
next = generator.throw(e);
}
}
return next.value;
})();
return [cancel, promise];
}
/**
* 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 t=50ms
*/