UNPKG

prex-es5

Version:

Async coordination primitives and extensions on top of ES6 Promises

374 lines (371 loc) 13 kB
/*! ***************************************************************************** Copyright (c) Microsoft Corporation. Licensed under the Apache License, Version 2.0. See LICENSE file in the project root for details. ***************************************************************************** */ import { LinkedList } from "./list"; import { isMissing, isBoolean, isFunction, isIterable, isInstance } from "./utils"; /** * Signals a CancellationToken that it should be canceled. */ export class CancellationTokenSource { /** * Initializes a new instance of a CancellationTokenSource. * * @param linkedTokens An optional iterable of tokens to which to link this source. */ constructor(linkedTokens) { this._state = "open"; this._token = undefined; this._registrations = undefined; this._linkingRegistrations = undefined; if (!isIterable(linkedTokens, /*optional*/ true)) throw new TypeError("Object not iterable: linkedTokens."); if (linkedTokens) { for (const linkedToken of linkedTokens) { if (!isInstance(linkedToken, CancellationToken)) throw new TypeError("CancellationToken expected."); if (linkedToken.cancellationRequested) { this._state = "cancellationRequested"; this._unlink(); break; } else if (linkedToken.canBeCanceled) { if (this._linkingRegistrations === undefined) { this._linkingRegistrations = []; } this._linkingRegistrations.push(linkedToken.register(() => this.cancel())); } } } } /** * Gets a CancellationToken linked to this source. */ get token() { if (this._token === undefined) { this._token = new CancellationToken(this); } return this._token; } /*@internal*/ get _currentState() { if (this._state === "open" && this._linkingRegistrations && this._linkingRegistrations.length > 0) { for (const registration of this._linkingRegistrations) { if (registration._cancellationSource && registration._cancellationSource._currentState === "cancellationRequested") { return "cancellationRequested"; } } } return this._state; } /** * Gets a value indicating whether cancellation has been requested. */ /*@internal*/ get _cancellationRequested() { return this._currentState === "cancellationRequested"; } /** * Gets a value indicating whether the source can be canceled. */ /*@internal*/ get _canBeCanceled() { return this._currentState !== "closed"; } /** * Cancels the source, evaluating any registered callbacks. If any callback raises an exception, * the exception is propagated to a host specific unhanedle exception mechanism. */ cancel() { if (this._state !== "open") { return; } this._state = "cancellationRequested"; this._unlink(); const registrations = this._registrations; this._registrations = undefined; if (registrations && registrations.size > 0) { for (const registration of registrations) { if (registration._cancellationTarget) { this._executeCallback(registration._cancellationTarget); } } } } /** * Closes the source, preventing the possibility of future cancellation. */ close() { if (this._state !== "open") { return; } this._state = "closed"; this._unlink(); const callbacks = this._registrations; this._registrations = undefined; if (callbacks !== undefined) { // The registration for each callback holds onto the node, the node holds onto the // list, and the list holds all other nodes and callbacks. By clearing the list, the // GC can collect any otherwise unreachable nodes. callbacks.clear(); } } /** * Registers a callback to execute when cancellation has been requested. If cancellation has * already been requested, the callback is executed immediately. * * @param callback The callback to register. */ /*@internal*/ _register(callback) { if (!isFunction(callback)) throw new TypeError("Function expected: callback."); if (this._state === "cancellationRequested") { this._executeCallback(callback); return emptyRegistration; } if (this._state === "closed") { return emptyRegistration; } if (this._registrations === undefined) { this._registrations = new LinkedList(); } const node = this._registrations.push({ _cancellationSource: this, _cancellationTarget: callback, unregister() { if (this._cancellationSource === undefined) return; if (this._cancellationSource._registrations) { this._cancellationSource._registrations.deleteNode(node); } this._cancellationSource = undefined; this._cancellationTarget = undefined; } }); return node.value; } /** * Executes the provided callback. * * @param callback The callback to execute. */ _executeCallback(callback) { try { callback(); } catch (e) { // HostReportError(e) setTimeout(() => { throw e; }, 0); } } /** * Unlinks the source from any linked tokens. */ _unlink() { const linkingRegistrations = this._linkingRegistrations; this._linkingRegistrations = undefined; if (linkingRegistrations !== undefined) { for (const linkingRegistration of linkingRegistrations) { linkingRegistration.unregister(); } } } } // A source that cannot be canceled. const closedSource = new CancellationTokenSource(); closedSource.close(); // A source that is already canceled. const canceledSource = new CancellationTokenSource(); canceledSource.cancel(); /** * Propagates notifications that operations should be canceled. */ export class CancellationToken { constructor(source) { if (isMissing(source)) { this._source = closedSource; } else if (isBoolean(source)) { this._source = source ? canceledSource : closedSource; } else { if (!isInstance(source, CancellationTokenSource)) throw new TypeError("CancellationTokenSource expected: source."); this._source = source; } Object.freeze(this); } /** * Gets a value indicating whether cancellation has been requested. */ get cancellationRequested() { return this._source._cancellationRequested; } /** * Gets a value indicating whether the underlying source can be canceled. */ get canBeCanceled() { return this._source._canBeCanceled; } /** * Adapts a CancellationToken-like primitive from a different library. */ static from(token) { if (isVSCodeCancellationTokenLike(token)) { if (token.isCancellationRequested) return CancellationToken.canceled; const source = new CancellationTokenSource(); token.onCancellationRequested(() => source.cancel()); return source.token; } else if (isAbortSignalLike(token)) { if (token.aborted) return CancellationToken.canceled; const source = new CancellationTokenSource(); token.addEventListener("abort", () => source.cancel()); return source.token; } else { return token; } } /** * Returns a CancellationToken that becomes canceled when **any** of the provided tokens are canceled. * @param tokens An iterable of CancellationToken objects. */ static race(tokens) { if (!isIterable(tokens)) throw new TypeError("Object not iterable: iterable."); const tokensArray = Array.isArray(tokens) ? tokens : [...tokens]; return tokensArray.length > 0 ? new CancellationTokenSource(tokensArray).token : CancellationToken.none; } /** * Returns a CancellationToken that becomes canceled when **all** of the provided tokens are canceled. * @param tokens An iterable of CancellationToken objects. */ static all(tokens) { if (!isIterable(tokens)) throw new TypeError("Object not iterable: iterable."); const tokensArray = Array.isArray(tokens) ? tokens : [...tokens]; return tokensArray.length > 0 ? new CancellationTokenCountdown(tokensArray).token : CancellationToken.none; } /** * Throws a CancelError if cancellation has been requested. */ throwIfCancellationRequested() { if (this.cancellationRequested) { throw new CancelError(); } } /** * Registers a callback to execute when cancellation is requested. * * @param callback The callback to register. */ register(callback) { return this._source._register(callback); } } /** * A token which will never be canceled. */ CancellationToken.none = new CancellationToken(/*canceled*/ false); /** * A token that is already canceled. */ CancellationToken.canceled = new CancellationToken(/*canceled*/ true); /** * An error thrown when an operation is canceled. */ export class CancelError extends Error { constructor(message) { super(message || "Operation was canceled"); } } CancelError.prototype.name = "CancelError"; const emptyRegistration = Object.create({ unregister() { } }); function isVSCodeCancellationTokenLike(token) { return typeof token === "object" && token !== null && isBoolean(token.isCancellationRequested) && isFunction(token.onCancellationRequested); } function isAbortSignalLike(token) { return typeof token === "object" && token !== null && isBoolean(token.aborted) && isFunction(token.addEventListener); } /** * An object that provides a CancellationToken that becomes cancelled when **all** of its * containing tokens are canceled. This is similar to `CancellationToken.all`, except that you are * able to add additional tokens. */ export class CancellationTokenCountdown { constructor(iterable) { this._addedCount = 0; this._signaledCount = 0; this._canBeSignaled = false; this._source = new CancellationTokenSource(); this._registrations = []; if (!isIterable(iterable, /*optional*/ true)) throw new TypeError("Object not iterable: iterable."); if (iterable) { for (const token of iterable) { this.add(token); } } this._canBeSignaled = true; this._checkSignalState(); } /** * Gets the number of tokens added to the countdown. */ get addedCount() { return this._addedCount; } /** * Gets the number of tokens that have not yet been canceled. */ get remainingCount() { return this._addedCount - this._signaledCount; } /** * Gets the CancellationToken for the countdown. */ get token() { return this._source.token; } /** * Adds a CancellationToken to the countdown. */ add(token) { if (!isInstance(token, CancellationToken)) throw new TypeError("CancellationToken expected."); if (this._source._currentState !== "open") return this; if (token.cancellationRequested) { this._addedCount++; this._signaledCount++; this._checkSignalState(); } else if (token.canBeCanceled) { this._addedCount++; this._registrations.push(token.register(() => { this._signaledCount++; this._checkSignalState(); })); } return this; } _checkSignalState() { if (!this._canBeSignaled || this._signaledCount < this._addedCount) return; this._canBeSignaled = false; if (this._addedCount > 0) { try { for (const registration of this._registrations) { registration.unregister(); } } finally { this._registrations.length = 0; this._source.cancel(); } } } } //# sourceMappingURL=cancellation.js.map