All files / utils/async deferred-value.ts

100% Statements 30/30
100% Branches 12/12
100% Functions 12/12
100% Lines 29/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195                                                                                                                                                                                                                                                        20x       81x 81x   81x 89x 89x 76x 2x     74x 74x     89x 14x 2x     12x   12x       81x   81x   136x       8x       9x       24x       76x       14x       9x 8x 8x   8x     9x       81x    
/**
 * A value the exposes a promise that can be resolved or rejected by external clients.
 *
 * Clients should cache the DeferredValue instance itself rather than its properties (like `promise`). This enables
 * proper usage of features like `renew()`, which may creates a new internal promise.
 *
 * @template T - The type of the promise's value.
 */
export interface DeferredValue<T> {
  /**
   * The promise that can be resolved or rejected.
   */
  readonly promise: Promise<T>;
 
  /**
   * Whether the promise has been resolved.
   */
  readonly resolved: boolean;
 
  /**
   * Whether the promise has been rejected.
   */
  readonly rejected: boolean;
 
  /**
   * Whether the promise has been settled, i.e., if it has been resolved or rejected.
   */
  readonly settled: boolean;
 
  /**
   * Resolves the deferred value's promise with a value. Calling this method has no effect if the deferred value is
   * already settled.
   */
  readonly resolve: (value: T | PromiseLike<T>) => void;
 
  /**
   * Rejects the deferred value's promise with a reason. Calling this method has no effect if the deferred value is
   * already settled.
   */
  readonly reject: (reason?: unknown) => void;
 
  /**
   * Resets a settled (i.e., resolved or rejected) promise to an unsettled state, allowing the same deferred value
   * instance to be used in a new asynchronous operation after the previous one has completed. Calling this method has
   * no effect if the deferred value is not settled (i.e., if its promise is neither resolved nor rejected.)
   *
   * @example Reusing a deferred value
   *
   * ```ts
   * const deferred = createDeferredValue<string>();
   *
   * // First use
   * deferred.resolve('first');
   * await deferred.promise; // resolves to "first"
   *
   * // Renew for second use
   * deferred.renew();
   * deferred.resolve('second');
   * await deferred.promise; // resolves to "second"
   * ```
   *
   * @example Chainable usage
   *
   * ```ts
   * const deferred = createDeferredValue<number>();
   * deferred.resolve(1);
   * await deferred.promise;
   *
   * // Chain renew and resolve
   * deferred.renew().resolve(2);
   * await deferred.promise; // resolves to 2
   * ```
   *
   * @returns The same deferred value instance, allowing for method chaining.
   */
  readonly renew: () => this;
}
 
/**
 * Creates deferred value that exposes a promise that can be resolved or rejected by external clients. This is useful
 * for scenarios where you need to control when a promise resolves or rejects from outside the promise's executor
 * function, such as in event-driven architectures, manual coordination of asynchronous operations, or implementing
 * custom waiting mechanisms.
 *
 * Clients should cache the DeferredValue instance itself rather than destructuring its properties (like `promise`).
 * This ensures proper usage of features like `renew()`, which may creates a new internal promise.
 *
 * @example Basic usage
 *
 * ```ts
 * // Create a deferred value to be resolved later
 * const deferred = createDeferredValue<string>();
 *
 * // Pass the promise to consumers that need to wait for the value
 * function waitForValue(): Promise<string> {
 *   return deferred.promise;
 * }
 *
 * // Later, resolve the promise when the value becomes available
 * function provideValue(value: string): void {
 *   deferred.resolve(value);
 * }
 * ```
 *
 * @example Using with event listeners
 *
 * ```ts
 * // Create a deferred that will be resolved when an event occurs
 * function waitForEvent(notifier: EventNotifier, eventName: string): Promise<any> {
 *   const deferred = createDeferredValue<any>();
 *
 *   const handler = (data: any) => {
 *     deferred.resolve(data);
 *     emitter.off(eventName, handler);
 *   };
 *
 *   notifier.onEvent(eventName, handler);
 *   return deferred.promise;
 * }
 * ```
 *
 * @template T The type of value that the promise will resolve to.
 * @returns An object containing the promise and functions to resolve or reject it.
 */
export const createDeferredValue = <T = void>(): DeferredValue<T> => {
  let resolve: (value: T | PromiseLike<T>) => void;
  let reject: (reason?: unknown) => void;
 
  let resolved = false;
  let rejected = false;
 
  const createPromise = () =>
    new Promise<T>((res, rej) => {
      resolve = (value) => {
        if (resolved || rejected) {
          return;
        }
 
        resolved = true;
        res(value);
      };
 
      reject = (reason) => {
        if (resolved || rejected) {
          return;
        }
 
        rejected = true;
        // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
        rej(reason);
      };
    });
 
  let promise = createPromise();
 
  const deferred: DeferredValue<T> = {
    get promise() {
      return promise;
    },
 
    get resolved() {
      return resolved;
    },
 
    get rejected() {
      return rejected;
    },
 
    get settled() {
      return resolved || rejected;
    },
 
    resolve: (value) => {
      resolve!(value);
    },
 
    reject: (reason) => {
      reject!(reason);
    },
 
    renew: () => {
      if (deferred.settled) {
        resolved = false;
        rejected = false;
 
        promise = createPromise();
      }
 
      return deferred;
    },
  };
 
  return deferred;
};