LeetCode 2821 - Delay the Resolution of Each Promise
The problem asks us to take an array of functions, each returning a promise, and a delay time ms. We are to return a new array of functions where invoking any function in this array returns a promise that behaves like the original promise but resolves or rejects only after an…
Difficulty: 🟡 Medium
Topics: —
Solution
Problem Understanding
The problem asks us to take an array of functions, each returning a promise, and a delay time ms. We are to return a new array of functions where invoking any function in this array returns a promise that behaves like the original promise but resolves or rejects only after an additional delay of ms milliseconds.
In other words, if the original promise resolves or rejects after t milliseconds, the new promise should resolve or reject after t + ms milliseconds. The order of promises must be preserved, and the original behavior (resolve or reject) should not change, only the timing should be shifted.
The input functions array has between 1 and 10 elements, and each element is guaranteed to be a function returning a promise. The delay ms ranges from 10 to 500. These constraints are relatively small, meaning we do not need to optimize for extremely large input sizes, but we must handle both resolution and rejection correctly. A potential edge case includes functions that immediately reject or resolve, which still need to be delayed correctly.
Approaches
The most straightforward approach is to wrap each function in a new function that adds a delay using setTimeout. We can call the original function, and then use a wrapper promise that waits for both the original promise to settle and for the ms delay before resolving or rejecting. This ensures the delay is applied without altering the original promise's outcome.
A key observation is that we do not need to chain multiple setTimeout calls or use complex scheduling. Each function can be independently wrapped, and JavaScript promises naturally handle the resolution/rejection sequence. Using Promise.allSettled or other batch mechanisms is unnecessary since each function is delayed independently.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) | O(n) | Wrap each function in a new promise with a delay, direct approach works efficiently for the small input size |
| Optimal | O(n) | O(n) | Same as brute force because the input size is small, no further optimization is needed; the optimal approach is simple wrapping |
Algorithm Walkthrough
- Initialize an empty array
delayedFunctionsto store the wrapped functions. - Iterate through each function
fnin the inputfunctionsarray. - For each
fn, create a new function that returns a promise. - Inside this new promise, call the original
fnand attach.thenand.catchhandlers. - In both the
.then(resolve) and.catch(reject) handlers, usesetTimeoutto delay the resolution or rejection bymsmilliseconds. - Push this wrapped function to the
delayedFunctionsarray. - Return
delayedFunctionsafter processing all functions.
Why it works: Each function is independently wrapped to delay the promise by ms milliseconds while preserving its original outcome. This guarantees both correctness (original behavior) and the required delay. The iteration preserves the order of the original array.
Python Solution
from typing import List, Callable
import asyncio
def delayAll(functions: List[Callable[[], asyncio.Future]], ms: int) -> List[Callable[[], asyncio.Future]]:
delayed_functions = []
for fn in functions:
def make_delayed(fn=fn):
async def delayed():
try:
result = await fn()
await asyncio.sleep(ms / 1000)
return result
except Exception as e:
await asyncio.sleep(ms / 1000)
raise e
return delayed
delayed_functions.append(make_delayed())
return delayed_functions
In this Python implementation, each function is wrapped using an asynchronous function delayed that awaits the original promise and then sleeps for ms milliseconds before returning the result or raising the error. The use of asyncio.sleep ensures a non-blocking delay. The make_delayed function ensures proper closure capture of the current fn.
Go Solution
package main
import (
"time"
)
func delayAll(functions []func() chan interface{}, ms int) []func() chan interface{} {
delayedFunctions := make([]func() chan interface{}, 0, len(functions))
for _, fn := range functions {
f := fn
delayed := func() chan interface{} {
resultChan := make(chan interface{})
go func() {
defer close(resultChan)
originalChan := f()
res := <-originalChan
time.Sleep(time.Duration(ms) * time.Millisecond)
resultChan <- res
}()
return resultChan
}
delayedFunctions = append(delayedFunctions, delayed)
}
return delayedFunctions
}
In Go, the implementation uses channels to represent promises. Each function is wrapped in a goroutine that reads the original channel, sleeps for the delay, and then sends the result to a new channel. This ensures non-blocking behavior and preserves the order of results.
Worked Examples
Example 1: functions = [() => Promise.resolve(30)], ms = 50
| Step | Action | State |
|---|---|---|
| 1 | Wrap the function | delayedFunctions[0] is a function returning a promise delayed by 50 ms |
| 2 | Call the delayed function | Promise waits for 30 ms (original) + 50 ms (delay) |
| 3 | Resolve | Total elapsed time = 80 ms, value = 30 |
Example 2: functions = [() => Promise.resolve(50), () => Promise.resolve(80)], ms = 70
| Step | Action | State |
|---|---|---|
| 1 | Wrap both functions | Each delayed function returns a promise delayed by 70 ms |
| 2 | Call the first function | Resolves after 50 + 70 = 120 ms |
| 3 | Call the second function | Resolves after 80 + 70 = 150 ms |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each function is wrapped once and invoked once |
| Space | O(n) | We create a new array of functions of the same size |
The complexity is straightforward as each function is handled independently, and the number of functions is small (1 to 10).
Test Cases
import asyncio
# test cases
async def test_delayAll():
f1 = lambda: asyncio.sleep(0.03, result=30)
f2 = lambda: asyncio.sleep(0.05, result=50)
f3 = lambda: asyncio.sleep(0.08, result=80)
delayed = delayAll([f1], 50)
res = await delayed[0]()
assert res == 30 # single function delayed
delayed2 = delayAll([f2, f3], 70)
res2 = await delayed2[0]()
res3 = await delayed2[1]()
assert res2 == 50 # first function resolves correctly
assert res3 == 80 # second function resolves correctly
| Test | Why |
|---|---|
| single delayed function | Checks basic delay functionality |
| multiple delayed functions | Ensures order and independent delays are correct |
Edge Cases
First, a function that immediately rejects: our implementation ensures that even rejections are delayed using the same asyncio.sleep, preserving both timing and error propagation. Second, multiple functions with different durations: each function is wrapped independently, so total elapsed times differ correctly. Third, the smallest possible delay (ms=10): the implementation still correctly delays using asyncio.sleep, ensuring even minimal delays are handled without race conditions. These cases prevent naive implementations from ignoring exceptions, misordering, or failing to respect the delay for immediate resolutions.