/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { CancellationTokenSource } from './cancellation.js'; import * as errors from './errors.js'; import { toDisposable } from './lifecycle.js'; export function isThenable(obj) { return obj && typeof obj.then === 'function'; } export function createCancelablePromise(callback) { const source = new CancellationTokenSource(); const thenable = callback(source.token); const promise = new Promise((resolve, reject) => { source.token.onCancellationRequested(() => { reject(errors.canceled()); }); Promise.resolve(thenable).then(value => { source.dispose(); resolve(value); }, err => { source.dispose(); reject(err); }); }); return new class { cancel() { source.cancel(); } then(resolve, reject) { return promise.then(resolve, reject); } catch(reject) { return this.then(undefined, reject); } finally(onfinally) { return promise.finally(onfinally); } }; } export function raceCancellation(promise, token, defaultValue) { return Promise.race([promise, new Promise(resolve => token.onCancellationRequested(() => resolve(defaultValue)))]); } /** * 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 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: * * const delayer = new Delayer(WAITING_PERIOD); * const letters = []; * * function letterReceived(l) { * letters.push(l); * delayer.trigger(() => { return makeTheTrip(); }); * } */ export class Delayer { constructor(defaultDelay) { this.defaultDelay = defaultDelay; this.timeout = null; this.completionPromise = null; this.doResolve = null; this.doReject = null; this.task = null; } trigger(task, delay = this.defaultDelay) { this.task = task; this.cancelTimeout(); if (!this.completionPromise) { this.completionPromise = new Promise((c, e) => { this.doResolve = c; this.doReject = e; }).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; } isTriggered() { return this.timeout !== null; } cancel() { this.cancelTimeout(); if (this.completionPromise) { if (this.doReject) { this.doReject(errors.canceled()); } this.completionPromise = null; } } cancelTimeout() { if (this.timeout !== null) { clearTimeout(this.timeout); this.timeout = null; } } dispose() { this.cancelTimeout(); } } export function timeout(millis, token) { if (!token) { return createCancelablePromise(token => timeout(millis, token)); } return new Promise((resolve, reject) => { const handle = setTimeout(resolve, millis); token.onCancellationRequested(() => { clearTimeout(handle); reject(errors.canceled()); }); }); } export function disposableTimeout(handler, timeout = 0) { const timer = setTimeout(handler, timeout); return toDisposable(() => clearTimeout(timer)); } export function first(promiseFactories, shouldStop = t => !!t, defaultValue = null) { let index = 0; const len = promiseFactories.length; const loop = () => { if (index >= len) { return Promise.resolve(defaultValue); } const factory = promiseFactories[index++]; const promise = Promise.resolve(factory()); return promise.then(result => { if (shouldStop(result)) { return Promise.resolve(result); } return loop(); }); }; return loop(); } export class TimeoutTimer { constructor(runner, timeout) { this._token = -1; if (typeof runner === 'function' && typeof timeout === 'number') { this.setIfNotSet(runner, timeout); } } dispose() { this.cancel(); } cancel() { if (this._token !== -1) { clearTimeout(this._token); this._token = -1; } } cancelAndSet(runner, timeout) { this.cancel(); this._token = setTimeout(() => { this._token = -1; runner(); }, timeout); } setIfNotSet(runner, timeout) { if (this._token !== -1) { // timer is already set return; } this._token = setTimeout(() => { this._token = -1; runner(); }, timeout); } } export class IntervalTimer { constructor() { this._token = -1; } dispose() { this.cancel(); } cancel() { if (this._token !== -1) { clearInterval(this._token); this._token = -1; } } cancelAndSet(runner, interval) { this.cancel(); this._token = setInterval(() => { runner(); }, interval); } } export class RunOnceScheduler { constructor(runner, timeout) { this.timeoutToken = -1; this.runner = runner; this.timeout = timeout; this.timeoutHandler = this.onTimeout.bind(this); } /** * Dispose RunOnceScheduler */ dispose() { this.cancel(); this.runner = null; } /** * Cancel current scheduled runner (if any). */ cancel() { if (this.isScheduled()) { clearTimeout(this.timeoutToken); this.timeoutToken = -1; } } /** * Cancel previous runner (if any) & schedule a new runner. */ schedule(delay = this.timeout) { this.cancel(); this.timeoutToken = setTimeout(this.timeoutHandler, delay); } /** * Returns true if scheduled. */ isScheduled() { return this.timeoutToken !== -1; } onTimeout() { this.timeoutToken = -1; if (this.runner) { this.doRun(); } } doRun() { if (this.runner) { this.runner(); } } } /** * Execute the callback the next time the browser is idle */ export let runWhenIdle; (function () { if (typeof requestIdleCallback !== 'function' || typeof cancelIdleCallback !== 'function') { const dummyIdle = Object.freeze({ didTimeout: true, timeRemaining() { return 15; } }); runWhenIdle = (runner) => { const handle = setTimeout(() => runner(dummyIdle)); let disposed = false; return { dispose() { if (disposed) { return; } disposed = true; clearTimeout(handle); } }; }; } else { runWhenIdle = (runner, timeout) => { const handle = requestIdleCallback(runner, typeof timeout === 'number' ? { timeout } : undefined); let disposed = false; return { dispose() { if (disposed) { return; } disposed = true; cancelIdleCallback(handle); } }; }; } })(); /** * An implementation of the "idle-until-urgent"-strategy as introduced * here: https://philipwalton.com/articles/idle-until-urgent/ */ export class IdleValue { constructor(executor) { this._didRun = false; this._executor = () => { try { this._value = executor(); } catch (err) { this._error = err; } finally { this._didRun = true; } }; this._handle = runWhenIdle(() => this._executor()); } dispose() { this._handle.dispose(); } get value() { if (!this._didRun) { this._handle.dispose(); this._executor(); } if (this._error) { throw this._error; } return this._value; } } //#endregion