blob: eff9b53533ab39a52a1b39bbeedd95b1bf73bb50 [file] [log] [blame]
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {Observable} from 'rxjs';
import {filter, take} from 'rxjs/operators';
/**
* @param fn An iteratee function to be passed each element of
* the array in order. Must return a promise, and the following
* iteration will not begin until resolution of the promise returned by
* the previous iteration.
*
* An optional second argument to fn is a callback that will halt the
* loop if called.
*/
export function asyncForeach<T>(
array: T[],
fn: (item: T, stopCallback: () => void) => Promise<unknown>
): Promise<T | void> {
if (!array.length) {
return Promise.resolve();
}
let stop = false;
const stopCallback = () => {
stop = true;
};
return fn(array[0], stopCallback).then(() => {
if (stop) {
return Promise.resolve();
}
return asyncForeach(array.slice(1), fn);
});
}
export const _testOnly_allTasks = new Map<number, DelayedTask>();
/**
* This is just a very simple and small wrapper around setTimeout(). Instead of
* the usual:
*
* const timer = window.setTimeout(() => {...do stuff...}, 123);
* window.clearTimeout(timer);
*
* With this class you can do:
*
* const task = new Task(() => {...do stuff...}, 123);
* task.cancel();
*
* It is just nicer to have an object for this instead of a number as a handle.
*/
export class DelayedTask {
private timer?: number;
constructor(private callback: () => void, waitMs = 0) {
this.timer = window.setTimeout(() => {
if (this.timer) _testOnly_allTasks.delete(this.timer);
this.timer = undefined;
if (this.callback) this.callback();
}, waitMs);
_testOnly_allTasks.set(this.timer, this);
}
cancel() {
if (this.isActive()) {
window.clearTimeout(this.timer);
if (this.timer) _testOnly_allTasks.delete(this.timer);
this.timer = undefined;
}
}
flush() {
if (this.isActive()) {
this.cancel();
if (this.callback) this.callback();
}
}
isActive() {
return this.timer !== undefined;
}
}
/**
* The usage pattern is:
*
* this.myDebouncedTask = debounce(this.myDebouncedTask, () => {...}, 123);
*
* It is identical to:
*
* this.myTask = new DelayedTask(() => {...}, 123);
*
* But it would cancel a potentially scheduled task beforehand.
*/
export function debounce(
existingTask: DelayedTask | undefined,
callback: () => void,
waitMs = 0
) {
existingTask?.cancel();
return new DelayedTask(callback, waitMs);
}
export interface DelayedPromise<T> extends Promise<T> {
resolve: (value?: T) => void;
reject: (reason?: unknown) => void;
cancel: () => void;
isActive: () => boolean;
}
function delayedPromise<T>(
callback: () => Promise<T>,
waitMs = 0
): DelayedPromise<T> {
let res: (value?: T) => void;
let rej: (reason?: unknown) => void;
let timer: number | undefined;
const promise = new Promise<T | undefined>((resolve, reject) => {
res = resolve;
rej = reject;
}) as DelayedPromise<T>;
promise.resolve = res!;
promise.reject = rej!;
promise.isActive = () => timer !== undefined;
promise.cancel = () => {
if (!promise.isActive()) return;
window.clearTimeout(timer);
timer = undefined;
};
timer = window.setTimeout(async () => {
timer = undefined;
try {
const result = await callback();
promise.resolve(result);
} catch (e) {
promise.reject(e);
}
}, waitMs);
return promise;
}
/**
* The usage pattern is
* this.aDebouncedPromise = debounceP(this.aDebouncedPromise, () => {...}, 123)
*/
export function debounceP<T>(
existingPromise: DelayedPromise<T> | undefined,
callback: () => Promise<T>,
waitMs = 0
): DelayedPromise<T> {
if (!existingPromise || !existingPromise.isActive()) {
return delayedPromise<T>(callback, waitMs);
} else {
existingPromise.cancel();
const promise = delayedPromise<T>(callback, waitMs);
promise
.then((v: T) => existingPromise.resolve(v))
.catch((e: unknown) => existingPromise.reject(e));
return promise;
}
}
const THROTTLE_INTERVAL_MS = 500;
/**
* Ensure only one call is made within THROTTLE_INTERVAL_MS and any call within
* this interval is ignored
*/
export function throttleWrap<T>(fn: (e: T) => void) {
let lastCall: number | undefined;
return (e: T) => {
if (
lastCall !== undefined &&
Date.now() - lastCall < THROTTLE_INTERVAL_MS
) {
return;
}
lastCall = Date.now();
fn(e);
};
}
/**
* Let's you wait for an Observable to become true.
*/
export function until<T>(obs$: Observable<T>, predicate: (t: T) => boolean) {
return new Promise<void>(resolve => {
obs$.pipe(filter(predicate), take(1)).subscribe(() => {
resolve();
});
});
}
export const isFalse = (b: boolean) => b === false;
export type PromiseResult<T> =
| {status: 'fulfilled'; value: T}
| {status: 'rejected'; reason: string};
export function isFulfilled<T>(
promiseResult?: PromiseResult<T>
): promiseResult is PromiseResult<T> & {status: 'fulfilled'} {
return promiseResult?.status === 'fulfilled';
}
// An equivalent to Promise.allSettled from ES2020.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
// TODO: Migrate our tooling to ES2020 and remove this method.
export function allSettled<T>(
promises: Promise<T>[]
): Promise<PromiseResult<T>[]> {
return Promise.all(
promises.map(promise =>
promise
.then(value => ({status: 'fulfilled', value} as const))
.catch(reason => ({status: 'rejected', reason} as const))
)
);
}