UNPKG

monaco-editor-core

Version:
938 lines • 30.6 kB
/*--------------------------------------------------------------------------------------------- * 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 { BugIndicatingError, CancellationError } from './errors.js'; import { isDisposable, toDisposable } from './lifecycle.js'; import { setTimeout0 } from './platform.js'; import { MicrotaskDelay } from './symbols.js'; export function isThenable(obj) { return !!obj && typeof obj.then === 'function'; } /** * Returns a promise that can be cancelled using the provided cancellation token. * * @remarks When cancellation is requested, the promise will be rejected with a {@link CancellationError}. * If the promise resolves to a disposable object, it will be automatically disposed when cancellation * is requested. * * @param callback A function that accepts a cancellation token and returns a promise * @returns A promise that can be cancelled */ export function createCancelablePromise(callback) { const source = new CancellationTokenSource(); const thenable = callback(source.token); let isCancelled = false; const promise = new Promise((resolve, reject) => { const subscription = source.token.onCancellationRequested(() => { isCancelled = true; subscription.dispose(); reject(new CancellationError()); }); Promise.resolve(thenable).then(value => { subscription.dispose(); source.dispose(); if (!isCancelled) { resolve(value); } else if (isDisposable(value)) { // promise has been cancelled, result is disposable and will // be cleaned up value.dispose(); } }, err => { subscription.dispose(); source.dispose(); reject(err); }); }); return new class { cancel() { source.cancel(); source.dispose(); } 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 new Promise((resolve, reject) => { const ref = token.onCancellationRequested(() => { ref.dispose(); resolve(defaultValue); }); promise.then(resolve, reject).finally(() => ref.dispose()); }); } /** * Returns a promise that rejects with an {@CancellationError} as soon as the passed token is cancelled. * @see {@link raceCancellation} */ export function raceCancellationError(promise, token) { return new Promise((resolve, reject) => { const ref = token.onCancellationRequested(() => { ref.dispose(); reject(new CancellationError()); }); promise.then(resolve, reject).finally(() => ref.dispose()); }); } /** * 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: * * const throttler = new Throttler(); * const letters = []; * * function deliver() { * const lettersToDeliver = letters; * letters = []; * return makeTheTrip(lettersToDeliver); * } * * function onLetterReceived(l) { * letters.push(l); * throttler.queue(deliver); * } */ export class Throttler { constructor() { this.activePromise = null; this.queuedPromise = null; this.queuedPromiseFactory = null; this.cancellationTokenSource = new CancellationTokenSource(); } queue(promiseFactory) { if (this.cancellationTokenSource.token.isCancellationRequested) { return Promise.reject(new Error('Throttler is disposed')); } if (this.activePromise) { this.queuedPromiseFactory = promiseFactory; if (!this.queuedPromise) { const onComplete = () => { this.queuedPromise = null; if (this.cancellationTokenSource.token.isCancellationRequested) { return; } const 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(this.cancellationTokenSource.token); return new Promise((resolve, reject) => { this.activePromise.then((result) => { this.activePromise = null; resolve(result); }, (err) => { this.activePromise = null; reject(err); }); }); } dispose() { this.cancellationTokenSource.cancel(); } } const timeoutDeferred = (timeout, fn) => { let scheduled = true; const handle = setTimeout(() => { scheduled = false; fn(); }, timeout); return { isTriggered: () => scheduled, dispose: () => { clearTimeout(handle); scheduled = false; }, }; }; const microtaskDeferred = (fn) => { let scheduled = true; queueMicrotask(() => { if (scheduled) { scheduled = false; fn(); } }); return { isTriggered: () => scheduled, dispose: () => { scheduled = false; }, }; }; /** * A helper to delay (debounce) 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.deferred = 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((resolve, reject) => { this.doResolve = resolve; this.doReject = reject; }).then(() => { this.completionPromise = null; this.doResolve = null; if (this.task) { const task = this.task; this.task = null; return task(); } return undefined; }); } const fn = () => { this.deferred = null; this.doResolve?.(null); }; this.deferred = delay === MicrotaskDelay ? microtaskDeferred(fn) : timeoutDeferred(delay, fn); return this.completionPromise; } isTriggered() { return !!this.deferred?.isTriggered(); } cancel() { this.cancelTimeout(); if (this.completionPromise) { this.doReject?.(new CancellationError()); this.completionPromise = null; } } cancelTimeout() { this.deferred?.dispose(); this.deferred = null; } dispose() { this.cancel(); } } /** * A helper to delay execution of a task that is being requested often, while * preventing accumulation of consecutive executions, while the task runs. * * The mail man is clever and waits for a certain amount of time, before going * out to deliver letters. While the mail man is going out, more letters arrive * and can only be delivered once he is back. Once he is back the mail man will * do one more trip to deliver the letters that have accumulated while he was out. */ export class ThrottledDelayer { constructor(defaultDelay) { this.delayer = new Delayer(defaultDelay); this.throttler = new Throttler(); } trigger(promiseFactory, delay) { return this.delayer.trigger(() => this.throttler.queue(promiseFactory), delay); } cancel() { this.delayer.cancel(); } dispose() { this.delayer.dispose(); this.throttler.dispose(); } } export function timeout(millis, token) { if (!token) { return createCancelablePromise(token => timeout(millis, token)); } return new Promise((resolve, reject) => { const handle = setTimeout(() => { disposable.dispose(); resolve(); }, millis); const disposable = token.onCancellationRequested(() => { clearTimeout(handle); disposable.dispose(); reject(new CancellationError()); }); }); } /** * Creates a timeout that can be disposed using its returned value. * @param handler The timeout handler. * @param timeout An optional timeout in milliseconds. * @param store An optional {@link DisposableStore} that will have the timeout disposable managed automatically. * * @example * const store = new DisposableStore; * // Call the timeout after 1000ms at which point it will be automatically * // evicted from the store. * const timeoutDisposable = disposableTimeout(() => {}, 1000, store); * * if (foo) { * // Cancel the timeout and evict it from store. * timeoutDisposable.dispose(); * } */ export function disposableTimeout(handler, timeout = 0, store) { const timer = setTimeout(() => { handler(); if (store) { disposable.dispose(); } }, timeout); const disposable = toDisposable(() => { clearTimeout(timer); store?.delete(disposable); }); store?.add(disposable); return disposable; } 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(); } /** * Processes tasks in the order they were scheduled. */ export class TaskQueue { constructor() { this._runningTask = undefined; this._pendingTasks = []; } /** * Waits for the current and pending tasks to finish, then runs and awaits the given task. * If the task is skipped because of clearPending, the promise is rejected with a CancellationError. */ schedule(task) { const deferred = new DeferredPromise(); this._pendingTasks.push({ task, deferred, setUndefinedWhenCleared: false }); this._runIfNotRunning(); return deferred.p; } _runIfNotRunning() { if (this._runningTask === undefined) { this._processQueue(); } } async _processQueue() { if (this._pendingTasks.length === 0) { return; } const next = this._pendingTasks.shift(); if (!next) { return; } if (this._runningTask) { throw new BugIndicatingError(); } this._runningTask = next.task; try { const result = await next.task(); next.deferred.complete(result); } catch (e) { next.deferred.error(e); } finally { this._runningTask = undefined; this._processQueue(); } } /** * Clears all pending tasks. Does not cancel the currently running task. */ clearPending() { const tasks = this._pendingTasks; this._pendingTasks = []; for (const task of tasks) { if (task.setUndefinedWhenCleared) { task.deferred.complete(undefined); } else { task.deferred.error(new CancellationError()); } } } } export class TimeoutTimer { constructor(runner, timeout) { this._isDisposed = false; this._token = undefined; if (typeof runner === 'function' && typeof timeout === 'number') { this.setIfNotSet(runner, timeout); } } dispose() { this.cancel(); this._isDisposed = true; } cancel() { if (this._token !== undefined) { clearTimeout(this._token); this._token = undefined; } } cancelAndSet(runner, timeout) { if (this._isDisposed) { throw new BugIndicatingError(`Calling 'cancelAndSet' on a disposed TimeoutTimer`); } this.cancel(); this._token = setTimeout(() => { this._token = undefined; runner(); }, timeout); } setIfNotSet(runner, timeout) { if (this._isDisposed) { throw new BugIndicatingError(`Calling 'setIfNotSet' on a disposed TimeoutTimer`); } if (this._token !== undefined) { // timer is already set return; } this._token = setTimeout(() => { this._token = undefined; runner(); }, timeout); } } export class IntervalTimer { constructor() { this.disposable = undefined; this.isDisposed = false; } cancel() { this.disposable?.dispose(); this.disposable = undefined; } cancelAndSet(runner, interval, context = globalThis) { if (this.isDisposed) { throw new BugIndicatingError(`Calling 'cancelAndSet' on a disposed IntervalTimer`); } this.cancel(); const handle = context.setInterval(() => { runner(); }, interval); this.disposable = toDisposable(() => { context.clearInterval(handle); this.disposable = undefined; }); } dispose() { this.cancel(); this.isDisposed = true; } } export class RunOnceScheduler { constructor(runner, delay) { this.timeoutToken = undefined; this.runner = runner; this.timeout = delay; 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 = undefined; } } /** * Cancel previous runner (if any) & schedule a new runner. */ schedule(delay = this.timeout) { this.cancel(); this.timeoutToken = setTimeout(this.timeoutHandler, delay); } get delay() { return this.timeout; } set delay(value) { this.timeout = value; } /** * Returns true if scheduled. */ isScheduled() { return this.timeoutToken !== undefined; } onTimeout() { this.timeoutToken = undefined; if (this.runner) { this.doRun(); } } doRun() { this.runner?.(); } } /** * Execute the callback the next time the browser is idle, returning an * {@link IDisposable} that will cancel the callback when disposed. This wraps * [requestIdleCallback] so it will fallback to [setTimeout] if the environment * doesn't support it. * * @param callback The callback to run when idle, this includes an * [IdleDeadline] that provides the time alloted for the idle callback by the * browser. Not respecting this deadline will result in a degraded user * experience. * @param timeout A timeout at which point to queue no longer wait for an idle * callback but queue it on the regular event loop (like setTimeout). Typically * this should not be used. * * [IdleDeadline]: https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline * [requestIdleCallback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback * [setTimeout]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout * * **Note** that there is `dom.ts#runWhenWindowIdle` which is better suited when running inside a browser * context */ export let runWhenGlobalIdle; export let _runWhenIdle; (function () { const safeGlobal = globalThis; if (typeof safeGlobal.requestIdleCallback !== 'function' || typeof safeGlobal.cancelIdleCallback !== 'function') { _runWhenIdle = (_targetWindow, runner, timeout) => { setTimeout0(() => { if (disposed) { return; } const end = Date.now() + 15; // one frame at 64fps const deadline = { didTimeout: true, timeRemaining() { return Math.max(0, end - Date.now()); } }; runner(Object.freeze(deadline)); }); let disposed = false; return { dispose() { if (disposed) { return; } disposed = true; } }; }; } else { _runWhenIdle = (targetWindow, runner, timeout) => { const handle = targetWindow.requestIdleCallback(runner, typeof timeout === 'number' ? { timeout } : undefined); let disposed = false; return { dispose() { if (disposed) { return; } disposed = true; targetWindow.cancelIdleCallback(handle); } }; }; } runWhenGlobalIdle = (runner, timeout) => _runWhenIdle(globalThis, runner, timeout); })(); export class AbstractIdleValue { constructor(targetWindow, executor) { this._didRun = false; this._executor = () => { try { this._value = executor(); } catch (err) { this._error = err; } finally { this._didRun = true; } }; this._handle = _runWhenIdle(targetWindow, () => 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; } get isInitialized() { return this._didRun; } } /** * An `IdleValue` that always uses the current window (which might be throttled or inactive) * * **Note** that there is `dom.ts#WindowIdleValue` which is better suited when running inside a browser * context */ export class GlobalIdleValue extends AbstractIdleValue { constructor(executor) { super(globalThis, executor); } } /** * Creates a promise whose resolution or rejection can be controlled imperatively. */ export class DeferredPromise { get isRejected() { return this.outcome?.outcome === 1 /* DeferredOutcome.Rejected */; } get isSettled() { return !!this.outcome; } constructor() { this.p = new Promise((c, e) => { this.completeCallback = c; this.errorCallback = e; }); } complete(value) { if (this.isSettled) { return Promise.resolve(); } return new Promise(resolve => { this.completeCallback(value); this.outcome = { outcome: 0 /* DeferredOutcome.Resolved */, value }; resolve(); }); } error(err) { if (this.isSettled) { return Promise.resolve(); } return new Promise(resolve => { this.errorCallback(err); this.outcome = { outcome: 1 /* DeferredOutcome.Rejected */, value: err }; resolve(); }); } cancel() { return this.error(new CancellationError()); } } //#endregion //#region Promises export var Promises; (function (Promises) { /** * A drop-in replacement for `Promise.all` with the only difference * that the method awaits every promise to either fulfill or reject. * * Similar to `Promise.all`, only the first error will be returned * if any. */ async function settled(promises) { let firstError = undefined; const result = await Promise.all(promises.map(promise => promise.then(value => value, error => { if (!firstError) { firstError = error; } return undefined; // do not rethrow so that other promises can settle }))); if (typeof firstError !== 'undefined') { throw firstError; } return result; // cast is needed and protected by the `throw` above } Promises.settled = settled; /** * A helper to create a new `Promise<T>` with a body that is a promise * itself. By default, an error that raises from the async body will * end up as a unhandled rejection, so this utility properly awaits the * body and rejects the promise as a normal promise does without async * body. * * This method should only be used in rare cases where otherwise `async` * cannot be used (e.g. when callbacks are involved that require this). */ function withAsyncBody(bodyFn) { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { try { await bodyFn(resolve, reject); } catch (error) { reject(error); } }); } Promises.withAsyncBody = withAsyncBody; })(Promises || (Promises = {})); export function createCancelableAsyncIterableProducer(callback) { const source = new CancellationTokenSource(); const innerIterable = callback(source.token); return new CancelableAsyncIterableProducer(source, async (emitter) => { const subscription = source.token.onCancellationRequested(() => { subscription.dispose(); source.dispose(); emitter.reject(new CancellationError()); }); try { for await (const item of innerIterable) { if (source.token.isCancellationRequested) { // canceled in the meantime return; } emitter.emitOne(item); } subscription.dispose(); source.dispose(); } catch (err) { subscription.dispose(); source.dispose(); emitter.reject(err); } }); } class ProducerConsumer { constructor() { this._unsatisfiedConsumers = []; this._unconsumedValues = []; } get hasFinalValue() { return !!this._finalValue; } produce(value) { this._ensureNoFinalValue(); if (this._unsatisfiedConsumers.length > 0) { const deferred = this._unsatisfiedConsumers.shift(); this._resolveOrRejectDeferred(deferred, value); } else { this._unconsumedValues.push(value); } } produceFinal(value) { this._ensureNoFinalValue(); this._finalValue = value; for (const deferred of this._unsatisfiedConsumers) { this._resolveOrRejectDeferred(deferred, value); } this._unsatisfiedConsumers.length = 0; } _ensureNoFinalValue() { if (this._finalValue) { throw new BugIndicatingError('ProducerConsumer: cannot produce after final value has been set'); } } _resolveOrRejectDeferred(deferred, value) { if (value.ok) { deferred.complete(value.value); } else { deferred.error(value.error); } } consume() { if (this._unconsumedValues.length > 0 || this._finalValue) { const value = this._unconsumedValues.length > 0 ? this._unconsumedValues.shift() : this._finalValue; if (value.ok) { return Promise.resolve(value.value); } else { return Promise.reject(value.error); } } else { const deferred = new DeferredPromise(); this._unsatisfiedConsumers.push(deferred); return deferred.p; } } } /** * Important difference to AsyncIterableObject: * If it is iterated two times, the second iterator will not see the values emitted by the first iterator. */ export class AsyncIterableProducer { constructor(executor, _onReturn) { this._onReturn = _onReturn; this._producerConsumer = new ProducerConsumer(); this._iterator = { next: () => this._producerConsumer.consume(), return: () => { this._onReturn?.(); return Promise.resolve({ done: true, value: undefined }); }, throw: async (e) => { this._finishError(e); return { done: true, value: undefined }; }, }; queueMicrotask(async () => { const p = executor({ emitOne: value => this._producerConsumer.produce({ ok: true, value: { done: false, value: value } }), emitMany: values => { for (const value of values) { this._producerConsumer.produce({ ok: true, value: { done: false, value: value } }); } }, reject: error => this._finishError(error), }); if (!this._producerConsumer.hasFinalValue) { try { await p; this._finishOk(); } catch (error) { this._finishError(error); } } }); } static fromArray(items) { return new AsyncIterableProducer((writer) => { writer.emitMany(items); }); } static fromPromise(promise) { return new AsyncIterableProducer(async (emitter) => { emitter.emitMany(await promise); }); } static fromPromisesResolveOrder(promises) { return new AsyncIterableProducer(async (emitter) => { await Promise.all(promises.map(async (p) => emitter.emitOne(await p))); }); } static merge(iterables) { return new AsyncIterableProducer(async (emitter) => { await Promise.all(iterables.map(async (iterable) => { for await (const item of iterable) { emitter.emitOne(item); } })); }); } static { this.EMPTY = AsyncIterableProducer.fromArray([]); } static map(iterable, mapFn) { return new AsyncIterableProducer(async (emitter) => { for await (const item of iterable) { emitter.emitOne(mapFn(item)); } }); } map(mapFn) { return AsyncIterableProducer.map(this, mapFn); } static coalesce(iterable) { return AsyncIterableProducer.filter(iterable, item => !!item); } coalesce() { return AsyncIterableProducer.coalesce(this); } static filter(iterable, filterFn) { return new AsyncIterableProducer(async (emitter) => { for await (const item of iterable) { if (filterFn(item)) { emitter.emitOne(item); } } }); } filter(filterFn) { return AsyncIterableProducer.filter(this, filterFn); } _finishOk() { if (!this._producerConsumer.hasFinalValue) { this._producerConsumer.produceFinal({ ok: true, value: { done: true, value: undefined } }); } } _finishError(error) { if (!this._producerConsumer.hasFinalValue) { this._producerConsumer.produceFinal({ ok: false, error: error }); } // Warning: this can cause to dropped errors. } [Symbol.asyncIterator]() { return this._iterator; } } export class CancelableAsyncIterableProducer extends AsyncIterableProducer { constructor(_source, executor) { super(executor); this._source = _source; } cancel() { this._source.cancel(); } } //#endregion export const AsyncReaderEndOfStream = Symbol('AsyncReaderEndOfStream'); //# sourceMappingURL=async.js.map