| /** |
| * @license |
| * Copyright 2017 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import {Observable} from 'rxjs'; |
| import {filter, take} from 'rxjs/operators'; |
| import {assertIsDefined} from './common-util'; |
| |
| /** |
| * @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 const DELAYED_CANCELLATION = Symbol('Delayed Cancellation'); |
| |
| export class DelayedPromise<T> extends Promise<T> { |
| private resolve: (value: PromiseLike<T> | T) => void; |
| |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| private reject: (reason?: any) => void; |
| |
| private timer: number | undefined; |
| |
| constructor(private readonly callback: () => Promise<T>, waitMs = 0) { |
| let resolve: ((value: PromiseLike<T> | T) => void) | undefined; |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| let reject: ((reason?: any) => void) | undefined; |
| super((res, rej) => { |
| resolve = res; |
| reject = rej; |
| }); |
| assertIsDefined(resolve); |
| assertIsDefined(reject); |
| this.resolve = resolve; |
| this.reject = reject; |
| this.timer = window.setTimeout(async () => { |
| await this.flush(); |
| }, waitMs); |
| } |
| |
| private stop() { |
| if (this.timer === undefined) return false; |
| window.clearTimeout(this.timer); |
| this.timer = undefined; |
| return true; |
| } |
| |
| async flush() { |
| if (!this.stop()) return; |
| try { |
| this.resolve(await this.callback()); |
| } catch (e) { |
| this.reject(e); |
| } |
| } |
| |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| cancel(reason?: any) { |
| if (!this.stop()) return; |
| this.reject(reason ?? DELAYED_CANCELLATION); |
| } |
| |
| delegate(other: Promise<T>) { |
| if (!this.stop()) return; |
| other |
| .then((value: T) => this.resolve(value)) |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| .catch((reason?: any) => this.reject(reason)); |
| } |
| |
| // From ECMAScript specification: |
| // https://tc39.es/ecma262/#sec-get-promise-@@species |
| // Promise prototype methods normally use their this value's constructor to |
| // create a derived object. However, a subclass constructor may over-ride |
| // that default behaviour by redefining its @@species property. |
| // NOTE: This is required otherwise .then and .catch on a DelayedPromise |
| // will try to instantiate a DelayedPromise with 'resolve, reject' arguments. |
| static override get [Symbol.species]() { |
| return Promise; |
| } |
| |
| override get [Symbol.toStringTag]() { |
| return 'DelayedPromise'; |
| } |
| } |
| |
| /** |
| * 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> { |
| const promise = new DelayedPromise<T>(callback, waitMs); |
| if (existingPromise) existingPromise.delegate(promise); |
| 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)) |
| ) |
| ); |
| } |