File size: 4,112 Bytes
6215321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

export enum PromiseResponseType {
  Resolved,
  Rejected,
  Expired
}

export type PromiseResponse<T> =
  {
    status: PromiseResponseType.Resolved
    value: T
    error: undefined
  } |
  {
    status: PromiseResponseType.Rejected
    value: undefined
    error: Error
  } |
  {
    status: PromiseResponseType.Expired
    value: undefined
    error: Error
  }

/**
 * This function waits for an array of promises to finish
 * 
 * Behavior:
 * 
 * As soon as any of the promises resolve,
 * this triggers an internal countdown (based on maxRunTimeAfterFirstResolveInMs)
 * after which the function will stop everything, and return the promises results.
 * Promises that finish after countdownAfterFirstResolveInMs will be ignored and their result discarded.
 * 
 * The result is an array matching the input number of promises, but with different status based on what happened:
 * 
 * Promises that resolved will be of type `{ status: PromiseResponseType.Resolved, value: ...... }`.
 * Promises that failed will be of type  `{ status: PromiseResponseType.Rejected, error: ...... }`.
 * Promises that failed will be of type  `{ status: PromiseResponseType.Expired, error: ...... }`.
 * 
 * Note that it is possible that no promises resolves at all,
 * which is why we also have a global expiration timeout (based on maxTotalRunTimeInMs) with more priority,
 * that will also stop the function and return the promises results (they will also have the expired status)
 * 
 * Note that maxTotalRunTimeInMs is reached, countdownAfterFirstResolveInMs becomes irrelevant,
 * so all promises that finish after maxTotalRunTimeInMs will be ignored and their result discarded.
 */
export async function waitPromisesUntil<T>(
  promises: Promise<T>[],
  countdownAfterFirstResolveInMs: number = 1000,
  maxTotalRunTimeInMs: number = 10000
): Promise<PromiseResponse<T>[]> {
  // This will store the result for each promise as it finishes or the operation is terminated.
  const results: PromiseResponse<T>[] = Array(promises.length).fill(undefined);
  /*{
    status: "expired",
    error: new Error("Promise expired")
  });
  */

  // Timer to handle maxTotalRunTimeInMs
  let globalTimeoutHandler: any;

  // Timer to handle countdownAfterFirstResolveInMs
  let firstResolveCountdownHandler: any;

  // Helper function to handle promise resolution
  const handleResolved = (index: number, value: T) => {
    if (!results[index]) { // If result isn't yet set
      results[index] = { status: PromiseResponseType.Resolved, value, error: undefined };
    }
    if (!firstResolveCountdownHandler) {
      // Start countdown after the first resolve
      firstResolveCountdownHandler = setTimeout(() => {
        finalizePromises(); // Stop accepting results after the countdown
      }, countdownAfterFirstResolveInMs);
    }
  };

  // Helper function to handle promise rejection
  const handleRejected = (index: number, error: any) => {
    if (!results[index]) {
      results[index] = { status: PromiseResponseType.Rejected, error, value: undefined };
    }
  };

  // Early finalization function to handle both timeouts
  const finalizePromises = () => {
    clearTimeout(globalTimeoutHandler);
    clearTimeout(firstResolveCountdownHandler);
    for (let i = 0; i < promises.length; i++) {
      if (!results[i]) { // Promises that have not been settled by either handler
        results[i] = { status: PromiseResponseType.Expired, error: new Error('Promise expired'), value: undefined };
      }
    }
  };

  // Wrap each promise with resolution and rejection handlers
  const watchedPromises = promises.map((promise, index) =>
    promise.then(
      value => handleResolved(index, value),
      error => handleRejected(index, error)
    )
  );

  // Set the global timeout
  globalTimeoutHandler = setTimeout(() => {
    finalizePromises();
  }, maxTotalRunTimeInMs);

  // Await all wrapped promises
  await Promise.all(watchedPromises.map(p => p.catch(() => {}))); // Catch errors to prevent early exits
  finalizePromises(); // Finalize to ensure all promises not settled are marked expired
  return results;
}