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…

LeetCode Problem 2821

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

  1. Initialize an empty array delayedFunctions to store the wrapped functions.
  2. Iterate through each function fn in the input functions array.
  3. For each fn, create a new function that returns a promise.
  4. Inside this new promise, call the original fn and attach .then and .catch handlers.
  5. In both the .then (resolve) and .catch (reject) handlers, use setTimeout to delay the resolution or rejection by ms milliseconds.
  6. Push this wrapped function to the delayedFunctions array.
  7. Return delayedFunctions after 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.