2795. Parallel Execution of Promises for Individual Results Retrieval
Problem Description
In this problem, we're given an array called functions
. Each element of this array is a function that, when called, returns a promise (fnPromise
). These promises can either resolve successfully (fulfilled) or fail to resolve (rejected).
We need to write a function promiseAllSettled
that receives the functions
array as its input and returns a single promise (promise
). This promise should ultimately resolve to an array of objects, with each object representing the outcome of one fnPromise
. The array should maintain the same order as the input functions in the functions
array.
The object for a fulfilled fnPromise
should have a format:
status: "fulfilled"
value: result
(whereresult
is the resolved value of thefnPromise
)
And the object for a rejected fnPromise
should have a format:
status: "rejected"
reason: error
(whereerror
is the error message obtained when thefnPromise
is rejected)
The challenge also specifically requests that we should not use the built-in Promise.allSettled()
method to accomplish this task.
Intuition
The core idea of our solution is to treat each function (which returns a promise) independently and keep track of each promise's outcome. This involves mapping each function to a promise that will always resolve—regardless of whether the original promise resolves or rejects. We do this because our main goal is to capture the fulfillment status and value or the rejection reason for every individual promise.
We iterate over the functions
array and for each function, we invoke it to get the fnPromise
. Then, we attach both .then
and .catch
handlers to understand if the fnPromise
has been fulfilled or rejected. Each of these handlers returns an object with the status and the corresponding value or reason. This aligns with the format required by our problem statement.
To construct the final array that the promise
will resolve with, we keep an array res
. We place the result object from either the .then
or .catch
handler into the res
array at the same index position as the original function in the functions
array. This ensures we maintain the order.
We also keep a count of how many promises have settled (either resolved or rejected). Once this count matches the length of the functions
array, we know all fnPromise
s have been settled, and we can resolve the outermost promise with the res
array as its value.
Solution Approach
The implementation of promiseAllSettled
function involves several steps. Let's break them down:
-
Initialization: We start by creating a new
Promise
object that will eventually be returned by thepromiseAllSettled
function. This outer promise resolves when all the individualfnPromise
s inside thefunctions
array have settled. -
Creating Result Array: We set up an array,
res
, that will accumulate the results of our promise settlements. It's crucial for this to be an array since we want to maintain the order of the results relative to the order of functions in thefunctions
array. -
Iteration: We use a
for
loop to iterate over thefunctions
array, denoted by the loop variablei
. It's important to note that we're using afor...in
loop, which iterates over the enumerable properties of the array (in this case, the indices). -
Executing Functions and Handling Promises: For each
functions[i]
, we call it to obtain afnPromise
. We then attach a.then
callback, which is used for a case where thefnPromise
resolves successfully. The.then
callback returns an object withstatus: 'fulfilled'
andvalue
as the resolved value of the promise.Likewise, we attach a
.catch
callback for the case where thefnPromise
rejects. The.catch
handler returns an object withstatus: 'rejected'
andreason
as the error message. Since.catch
is chained after.then
, an error will bypass the.then
and be caught by the.catch
. -
Storing Results: The result object obtained from either the
.then
or.catch
is placed into theres
array at the position matching thefunctions
array. This step usesi
, which represents the current index in thefunctions
array, to ensure the result order is the same as the input function order. -
Tracking Completion: We maintain a count of how many functions have settled. Each time a function settles (either resolved or rejected), we increment the
count
. Whencount
matches the length of thefunctions
array, we know all promises have settled and we can resolve the outer promise with theres
array.
The use of .then
and .catch
chained properly ensures that even if a promise rejects, it's converted into a resolve for our outer promise, so that a single rejection doesn't cause the whole process to stop. Instead, it's treated as a regular outcome and managed accordingly.
By only resolving after all promises have settled, we emulate the behavior of Promise.allSettled
without using it directly. This allows us to gather all results and understand whether each individual promise was fulfilled or rejected.
In summary, the algorithm capitalizes on the asynchronous nature of promises in JavaScript, handling them independently and aggregating their results in a way that maintains their original order.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's consider an example where we have three functions in our functions
array: fn1
, fn2
, and fn3
. Function fn1
resolves immediately, fn2
rejects after a delay, and fn3
resolves after a delay.
const fn1 = () => Promise.resolve('Result of fn1');
const fn2 = () => new Promise((_, reject) => setTimeout(() => reject('Error in fn2'), 100));
const fn3 = () => new Promise(resolve => setTimeout(() => resolve('Result of fn3'), 100));
const functions = [fn1, fn2, fn3];
We call promiseAllSettled(functions)
to process these functions. Let's walk through what happens step by step following the solution approach:
-
Initialization: A new
Promise
is created which will be returned bypromiseAllSettled
. This will resolve after allfnPromise
s fromfunctions
have settled. -
Creating Result Array: An empty array
res
is created to accumulate results. -
Iteration: A
for
loop (or similar iteration mechanism) iterates over the indices offunctions
. -
Executing Functions and Handling Promises:
-
fn1
is called, returning a promise that resolves immediately. The.then
callback fires and creates a result object:{status: 'fulfilled', value: 'Result of fn1'}
. The.catch
is not invoked because there's no error. -
fn2
is called, producing a promise that will reject after 100ms. When it rejects, the.catch
handler fires and creates a result object:{status: 'rejected', reason: 'Error in fn2'}
. -
fn3
is called, returning a promise that resolves after 100ms. Once it resolves, the.then
callback creates a result object:{status: 'fulfilled', value: 'Result of fn3'}
.
-
-
Storing Results: The objects created in the last step are placed into the
res
array. After all callbacks have been executed,res
looks like this:
[ { status: 'fulfilled', value: 'Result of fn1' }, { status: 'rejected', reason: 'Error in fn2' }, { status: 'fulfilled', value: 'Result of fn3' } ]
- Tracking Completion: We keep a
count
of settled functions. In our case, every function call will eventually trigger its respective.then
or.catch
handler, incrementing thecount
. Oncecount
is equal tofunctions.length
(3 in this case), we resolve the outer promise with theres
array.
After waiting for all promises to settle, promiseAllSettled
returns a promise that resolves to res
, maintaining the order of functions
. As such, the final result for promiseAllSettled(functions)
would be:
[ { status: 'fulfilled', value: 'Result of fn1' }, { status: 'rejected', reason: 'Error in fn2' }, { status: 'fulfilled', value: 'Result of fn3' } ]
This example clearly illustrates how the solution approach operates. Even though fn2
fails, fn1
and fn3
still contribute their results, and we end up with an array describing the status of each promise as per the original order in the functions
array.
Solution Implementation
1import asyncio
2
3# Define Python classes for both fulfilled and rejected promise outcomes.
4class FulfilledObj:
5 def __init__(self, value):
6 self.status = 'fulfilled'
7 self.value = value
8
9class RejectedObj:
10 def __init__(self, reason):
11 self.status = 'rejected'
12 self.reason = reason
13
14# Union type hint for a resolved promise outcome, which can be either fulfilled or rejected.
15Outcome = FulfilledObj | RejectedObj
16
17# Function that simulates promiseAllSettled for an array of coroutines.
18# It returns a list of objects indicating the outcome of each coroutine.
19async def promise_all_settled(functions):
20 # List to hold the outcomes of each coroutine.
21 results = [None] * len(functions)
22 # Control flow task for fulfilling all coroutines.
23 tasks = []
24
25 # Handle fulfilled coroutine.
26 async def handle_fulfilled(index, coro):
27 try:
28 value = await coro
29 results[index] = FulfilledObj(value)
30 except Exception as e: # Replace Exception by a more specific exception class if possible
31 results[index] = RejectedObj(e)
32
33 # Iterate through the coroutine list and schedule each coroutine.
34 for index, func in enumerate(functions):
35 task = asyncio.create_task(handle_fulfilled(index, func()))
36 tasks.append(task)
37
38 # Await all scheduled tasks to be finished.
39 await asyncio.gather(*tasks)
40 return results
41
42# Example usage of the `promise_all_settled` function.
43# Uncomment the following code to test the functionality.
44"""
45async def main():
46 functions = [
47 lambda: asyncio.sleep(0.1, result='Result')
48 ]
49
50 # A simple timer to measure elapsed time.
51 start_time = asyncio.get_event_loop().time()
52
53 results = await promise_all_settled(functions)
54
55 elapsed_time = asyncio.get_event_loop().time() - start_time
56 output = {'elapsed': elapsed_time, 'values': results}
57 print(output) # Expected output with an elapsed time close to 0.1 and the 'Result'
58
59# Run the example
60asyncio.run(main())
61"""
62
1import java.util.concurrent.CompletableFuture;
2import java.util.concurrent.ExecutionException;
3import java.util.function.Supplier;
4import java.util.List;
5import java.util.ArrayList;
6
7// Java does not have type unions like TypeScript, so we use an interface with subclasses for fulfilled and rejected promise outcomes.
8interface ResultObj {}
9class FulfilledObj implements ResultObj {
10 String status;
11 Object value;
12
13 public FulfilledObj(Object value) {
14 this.status = "fulfilled";
15 this.value = value;
16 }
17}
18class RejectedObj implements ResultObj {
19 String status;
20 Object reason;
21
22 public RejectedObj(Object reason) {
23 this.status = "rejected";
24 this.reason = reason;
25 }
26}
27
28public class PromiseAllSettled {
29
30 // Function that simulates Promise.allSettled for an array of suppliers that return CompletableFuture.
31 // It returns a CompletableFuture that resolves to a list of objects indicating the outcome of each CompletableFuture.
32 public static CompletableFuture<List<ResultObj>> promiseAllSettled(List<Supplier<CompletableFuture<Object>>> suppliers) {
33 // List to hold the outcomes of each CompletableFuture.
34 List<CompletableFuture<ResultObj>> futures = new ArrayList<>();
35
36 // Convert each supplier to a CompletableFuture and handle its resolution accordingly.
37 for (Supplier<CompletableFuture<Object>> supplier : suppliers) {
38 CompletableFuture<ResultObj> future = supplier.get()
39 .thenApply(value -> new FulfilledObj(value))
40 .exceptionally(ex -> new RejectedObj(ex));
41 futures.add(future);
42 }
43
44 // Combine all CompletableFuture instances into a single CompletableFuture that will be resolved when all are completed.
45 CompletableFuture<Void> allDoneFuture = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
46
47 // Return a CompletableFuture that resolves to a list of ResultObj instances representing the outcome of each.
48 return allDoneFuture.thenApply(v -> {
49 List<ResultObj> results = new ArrayList<>();
50 futures.forEach(f -> {
51 try {
52 results.add(f.get());
53 } catch (InterruptedException | ExecutionException e) {
54 // This should never happen since we're calling get after all futures are done.
55 throw new RuntimeException(e);
56 }
57 });
58 return results;
59 });
60 }
61
62 // Example usage of the `promiseAllSettled` function.
63 public static void main(String[] args) throws ExecutionException, InterruptedException {
64 List<Supplier<CompletableFuture<Object>>> suppliers = new ArrayList<>();
65 suppliers.add(() -> CompletableFuture.supplyAsync(() -> {
66 // Simulate a task that takes some time to complete
67 try {
68 Thread.sleep(100);
69 } catch (InterruptedException e) {
70 e.printStackTrace();
71 }
72 return "Result";
73 }));
74
75 CompletableFuture<List<ResultObj>> promise = promiseAllSettled(suppliers);
76
77 // Start timing
78 long startTime = System.currentTimeMillis();
79
80 // When all CompletableFutures are settled, print the outcome.
81 promise.thenAccept(res -> {
82 long elapsedTime = System.currentTimeMillis() - startTime;
83 System.out.println("Elapsed time: " + elapsedTime + "ms");
84 for (ResultObj obj : res) {
85 if (obj instanceof FulfilledObj) {
86 FulfilledObj fulfilled = (FulfilledObj) obj;
87 System.out.println("Status: " + fulfilled.status + ", Value: " + fulfilled.value.toString());
88 } else if (obj instanceof RejectedObj) {
89 RejectedObj rejected = (RejectedObj) obj;
90 System.out.println("Status: " + rejected.status + ", Reason: " + rejected.reason.toString());
91 }
92 }
93 }).get(); // Waiting for completion to ensure that the results are printed out.
94 }
95}
96
1#include <future>
2#include <vector>
3#include <functional>
4#include <iostream>
5#include <chrono>
6
7// Define C++ equivalent types for both fulfilled and rejected future outcomes.
8struct FulfilledObj {
9 std::string status = "fulfilled";
10 std::shared_ptr<void> value; // Using shared_ptr for type-erased generic value
11};
12
13struct RejectedObj {
14 std::string status = "rejected";
15 std::string reason; // Assuming the reason will be string for simplicity
16};
17
18// Union-like structure that can hold either outcome.
19struct OutcomeObj {
20 std::string status;
21 std::shared_ptr<void> value; // For 'fulfilled'
22 std::string reason; // For 'rejected'
23};
24
25// Function that simulates Promise.allSettled for an array of functions that return futures.
26// This function will return a future that resolves to a vector of OutcomeObj, representing each promise's outcome.
27std::future<std::vector<OutcomeObj>> promiseAllSettled(
28 const std::vector<std::function<std::future<void>()>>& functions) {
29
30 return std::async(std::launch::async, [functions]{
31 std::vector<OutcomeObj> outcomes;
32 // Reserve space for outcome objects based on the number of input functions.
33 outcomes.reserve(functions.size());
34
35 // Iterate through the function array and execute each function.
36 for (const auto& func : functions) {
37 // Attempt to fulfill the future and handle the outcome accordingly.
38 try {
39 // Wait for the future to complete and store a FulfilledObj.
40 std::shared_ptr<void> result = func().get();
41 outcomes.push_back(OutcomeObj{"fulfilled", result, ""});
42 }
43 catch (const std::exception& e) {
44 // If an exception is thrown, store a RejectedObj.
45 outcomes.push_back(OutcomeObj{"rejected", nullptr, e.what()});
46 }
47 }
48
49 // All futures are settled at this point, return the outcomes.
50 return outcomes;
51 });
52}
53
54// Example usage of the `promiseAllSettled` function.
55int main() {
56 // Create a vector of functions that return futures.
57 std::vector<std::function<std::future<void>()>> functions = {
58 // Example function that fulfills the future after a delay.
59 [] {
60 return std::async(std::launch::async, [] {
61 std::this_thread::sleep_for(std::chrono::milliseconds(100));
62 // In this example, return type is void. Type erasure can be done to hold any type.
63 return;
64 });
65 }
66 };
67
68 // Record start time.
69 auto startTime = std::chrono::high_resolution_clock::now();
70
71 // Call `promiseAllSettled` and wait for all futures to settle.
72 auto outcomesFuture = promiseAllSettled(functions);
73 outcomesFuture.wait(); // Wait for all futures to complete.
74
75 // Calculate the elapsed time.
76 auto endTime = std::chrono::high_resolution_clock::now();
77 auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
78
79 // Retrieve the outcomes and print the results.
80 std::vector<OutcomeObj> outcomes = outcomesFuture.get();
81 for (const auto& outcome : outcomes) {
82 if (outcome.status == "fulfilled") {
83 std::cout << "{\"status\":\"" << outcome.status << "\",\"value\":\"Result\"}" << std::endl;
84 } else {
85 std::cout << "{\"status\":\"" << outcome.status << "\",\"reason\":\"" << outcome.reason << "\"}" << std::endl;
86 }
87 }
88
89 std::cout << "Elapsed Time: " << elapsedTime << "ms\n";
90 return 0;
91}
92
1// Define TypeScript types for both fulfilled and rejected promise outcomes.
2type FulfilledObj = {
3 status: 'fulfilled';
4 value: unknown;
5};
6type RejectedObj = {
7 status: 'rejected';
8 reason: unknown;
9};
10type Obj = FulfilledObj | RejectedObj;
11
12// Function that simulates Promise.allSettled for an array of functions that return promises.
13// It returns a Promise that resolves to an array of objects indicating the outcome of each promise.
14function promiseAllSettled(functions: (() => Promise<unknown>)[]): Promise<Obj[]> {
15 return new Promise(resolve => {
16 // Array to hold the outcomes of each promise.
17 const results: Obj[] = [];
18 // Counter to keep track of resolved promises.
19 let completedCount = 0;
20
21 // Iterate through the function array and execute each function.
22 functions.forEach((func, index) => {
23 // Assume that each function returns a promise and handle its resolution accordingly.
24 func()
25 .then(value => ({ status: 'fulfilled', value } as FulfilledObj))
26 .catch(reason => ({ status: 'rejected', reason } as RejectedObj))
27 .then(obj => {
28 // When a promise is settled (either fulfilled or rejected), store its result.
29 results[index] = obj;
30 // Increment the number of completed promises.
31 completedCount++;
32 // When all promises have been settled, resolve the outer promise with the results array.
33 if (completedCount === functions.length) {
34 resolve(results);
35 }
36 });
37 });
38 });
39}
40
41// Example usage of the `promiseAllSettled` function.
42// Uncomment the following code to test the functionality.
43/*
44const functions = [
45 () => new Promise(resolve => setTimeout(() => resolve('Result'), 100))
46];
47const startTime = performance.now();
48
49const promise = promiseAllSettled(functions);
50
51promise.then(res => {
52 const elapsedTime = Math.floor(performance.now() - startTime);
53 const output = { elapsed: elapsedTime, values: res };
54 console.log(output); // Expected output: {"elapsed":100,"values":[{"status":"fulfilled","value":"Result"}]}
55});
56*/
57
Time and Space Complexity
Time Complexity
The time complexity of the promiseAllSettled
function is primarily determined by the functions[i]()
Promises execution. Assuming that each function returns a Promise that settles in at most time T
, the promiseAllSettled
function has a time complexity of O(T)
. It's because Promise.allSettled
executes all Promises concurrently, and the overall time taken depends on the longest-running Promise rather than the total count of Promises.
However, note that if the execution time T
varies for the Promises, the time complexity in relation to the number of Promises n
does not strictly apply as Promises are not executed sequentially. The time complexity in terms of concurrency is determined by the "slowest" or longest-running asynchronous operation.
Space Complexity
The space complexity of the promiseAllSettled
function is O(n)
, where n
is the number of functions (Promises) passed to it. This is because a result object (Obj
) is stored for each Promise, and these are accumulated in the res
array. The size of this array grows linearly with the number of Promises provided, so it scales linearly with the input size.
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
Recommended Readings
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!