blob: 981bcaeeb57de40515ecfc22ee3c3e932e56eb31 [file] [log] [blame]
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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);
}
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))
)
);
}