// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import { CancellationTokenSource, Disposable } from "vscode"; /** * Utilities for working with promises. */ export class PromiseUtil { public static async forEach( sources: T[], promiseGenerator: (source: T) => Promise, ): Promise { await Promise.all(sources.map(source => promiseGenerator(source))); } /** * Retries an operation a given number of times. For each retry, a condition is checked. * If the condition is not satisfied after the maximum number of retries, and error is thrown. * Otherwise, the result of the operation is returned once the condition is satisfied. * * @param operation - the function to execute. * @param condition - the condition to check between iterations. * @param maxRetries - the maximum number of retries. * @param delay - time between iterations, in milliseconds. * @param failure - error description. */ public static retryAsync( operation: () => Promise, condition: (result: T) => boolean | Promise, maxRetries: number, delay: number, failure: string, cancellationTokenSource?: CancellationTokenSource, ): Promise { return this.retryAsyncIteration( operation, condition, maxRetries, 0, delay, failure, cancellationTokenSource, ); } public static async reduce( sources: T[] | Promise, generateAsyncOperation: (value: T) => Promise, ): Promise { const arraySources: T[] = sources instanceof Promise ? await sources : sources; return arraySources.reduce(async (previousReduction: Promise, newSource: T) => { await previousReduction; return generateAsyncOperation(newSource); }, Promise.resolve()); } public static async delay(duration: number): Promise { return new Promise(resolve => { setTimeout(resolve, duration); }); } public static waitUntil( condition: () => Promise | T | null, interval: number = 1000, timeout?: number, ): Promise { return new Promise(async (resolve, reject) => { let rejectTimeout: NodeJS.Timeout | undefined; // eslint-disable-next-line prefer-const let сheckInterval: NodeJS.Timeout | undefined; if (timeout) { rejectTimeout = setTimeout(() => { // eslint-disable-next-line @typescript-eslint/no-use-before-define cleanup(); resolve(null); }, timeout); } const cleanup = () => { if (rejectTimeout) { clearTimeout(rejectTimeout); } if (сheckInterval) { clearInterval(сheckInterval); } }; const tryToResolve = async (): Promise => { try { const result = await condition(); if (result) { cleanup(); resolve(result); } return !!result; } catch (err) { cleanup(); reject(err); return false; } }; const resolved = await tryToResolve(); if (resolved) { return; } сheckInterval = setInterval(async () => { await tryToResolve(); }, interval); }); } public static promiseCacheDecorator( func: (...args: any[]) => Promise, context: Record | null = null, ): (...args: any[]) => Promise { let promise: Promise | undefined; return (...args: any[]): Promise => { if (!promise) { promise = func.apply(context, args) as Promise; } return promise; }; } private static async retryAsyncIteration( operation: () => Promise, condition: (result: T) => boolean | Promise, maxRetries: number, iteration: number, delay: number, failure: string, cancellationTokenSource?: CancellationTokenSource, ): Promise { const result = await operation(); const conditionResult = await condition(result); if (conditionResult) { return result; } if ( iteration < maxRetries && !(cancellationTokenSource && cancellationTokenSource.token.isCancellationRequested) ) { await PromiseUtil.delay(delay); return this.retryAsyncIteration( operation, condition, maxRetries, iteration + 1, delay, failure, cancellationTokenSource, ); } throw new Error(failure); } } export class Delayer implements Disposable { private timeout: any; private completionPromise: Promise | null; private doResolve: ((value?: any | Promise) => void) | null; private doReject: ((err: any) => void) | null; private task: { (): T | Promise } | null; constructor() { this.timeout = null; this.completionPromise = null; this.doResolve = null; this.doReject = null; this.task = null; } public runWihtDelay(task: { (): T | Promise }, delay: number): Promise { this.task = task; this.cancelTimeout(); if (!this.completionPromise) { this.completionPromise = new Promise((resolve, reject) => { this.doResolve = resolve; this.doReject = reject; }).then(() => { this.completionPromise = null; this.doResolve = null; if (this.task) { const task = this.task; this.task = null; return task(); } return undefined; }); } this.timeout = setTimeout(() => { this.timeout = null; if (this.doResolve) { this.doResolve(null); } }, delay); return this.completionPromise; } public isRunning(): boolean { return this.timeout !== null; } public cancel(): void { this.cancelTimeout(); if (this.completionPromise) { if (this.doReject) { this.doReject(new Error("Canceled")); } this.completionPromise = null; } } public dispose(): void { this.cancel(); } private cancelTimeout(): void { if (this.timeout !== null) { clearTimeout(this.timeout); this.timeout = null; } } }