/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ export interface ITask { (): T; } /** * A helper to prevent accumulation of sequential async tasks. * * Imagine a mail man with the sole task of delivering letters. As soon as * a letter submitted for delivery, he drives to the destination, delivers it * and returns to his base. Imagine that during the trip, N more letters were submitted. * When the mail man returns, he picks those N letters and delivers them all in a * single trip. Even though N+1 submissions occurred, only 2 deliveries were made. * * The throttler implements this via the queue() method, by providing it a task * factory. Following the example: * * var throttler = new Throttler(); * var letters = []; * * function letterReceived(l) { * letters.push(l); * throttler.queue(() => { return makeTheTrip(); }); * } */ export class Throttler { private activePromise: Promise | null; private queuedPromise: Promise | null; private queuedPromiseFactory: ITask> | null; constructor() { this.activePromise = null; this.queuedPromise = null; this.queuedPromiseFactory = null; } public queue(promiseFactory: ITask>): Promise { if (this.activePromise) { this.queuedPromiseFactory = promiseFactory; if (!this.queuedPromise) { let onComplete = () => { this.queuedPromise = null; let result = this.queue(this.queuedPromiseFactory!); this.queuedPromiseFactory = null; return result; }; this.queuedPromise = new Promise((resolve) => { this.activePromise!.then(onComplete, onComplete).then(resolve); }); } return new Promise((resolve, reject) => { this.queuedPromise!.then(resolve, reject); }); } this.activePromise = promiseFactory(); return new Promise((resolve, reject) => { this.activePromise!.then((result: T) => { this.activePromise = null; resolve(result); }, (err: any) => { this.activePromise = null; reject(err); }); }); } } /** * A helper to delay execution of a task that is being requested often. * * Following the throttler, now imagine the mail man wants to optimize the number of * trips proactively. The trip itself can be long, so the he decides not to make the trip * as soon as a letter is submitted. Instead he waits a while, in case more * letters are submitted. After said waiting period, if no letters were submitted, he * decides to make the trip. Imagine that N more letters were submitted after the first * one, all within a short period of time between each other. Even though N+1 * submissions occurred, only 1 delivery was made. * * The delayer offers this behavior via the trigger() method, into which both the task * to be executed and the waiting period (delay) must be passed in as arguments. Following * the example: * * var delayer = new Delayer(WAITING_PERIOD); * var letters = []; * * function letterReceived(l) { * letters.push(l); * delayer.trigger(() => { return makeTheTrip(); }); * } */ export class Delayer { public defaultDelay: number; private timeout: NodeJS.Timer | null; private completionPromise: Promise | null; private onResolve: ((value: T | PromiseLike | undefined) => void) | null; private task: ITask | null; constructor(defaultDelay: number) { this.defaultDelay = defaultDelay; this.timeout = null; this.completionPromise = null; this.onResolve = null; this.task = null; } public trigger(task: ITask, delay: number = this.defaultDelay): Promise { this.task = task; this.cancelTimeout(); if (!this.completionPromise) { this.completionPromise = new Promise((resolve) => { this.onResolve = resolve; }).then(() => { this.completionPromise = null; this.onResolve = null; let result = this.task!(); this.task = null; return result; }); } this.timeout = setTimeout(() => { this.timeout = null; this.onResolve!(undefined); }, delay); return this.completionPromise; } public isTriggered(): boolean { return this.timeout !== null; } public cancel(): void { this.cancelTimeout(); if (this.completionPromise) { this.completionPromise = null; } } private cancelTimeout(): void { if (this.timeout !== null) { clearTimeout(this.timeout); this.timeout = null; } } } /** * A helper to delay execution of a task that is being requested often, while * preventing accumulation of consecutive executions, while the task runs. * * Simply combine the two mail man strategies from the Throttler and Delayer * helpers, for an analogy. */ export class ThrottledDelayer extends Delayer> { private throttler: Throttler; constructor(defaultDelay: number) { super(defaultDelay); this.throttler = new Throttler(); } public override trigger(promiseFactory: ITask>, delay?: number): Promise> { return super.trigger(() => this.throttler.queue(promiseFactory), delay); } }