/* _ __ _ __ | | / /___ _(_) /____ | | /| / / __ `/ / / ___/ | |/ |/ / /_/ / / (__ ) |__/|__/\__,_/_/_/____/ The electron alternative for Go (c) Lea Anthony 2019-present */ var _a; import isCallable from "./callable.js"; /** * Exception class that will be used as rejection reason * in case a {@link CancellablePromise} is cancelled successfully. * * The value of the {@link name} property is the string `"CancelError"`. * The value of the {@link cause} property is the cause passed to the cancel method, if any. */ export class CancelError extends Error { /** * Constructs a new `CancelError` instance. * @param message - The error message. * @param options - Options to be forwarded to the Error constructor. */ constructor(message, options) { super(message, options); this.name = "CancelError"; } } /** * Exception class that will be reported as an unhandled rejection * in case a {@link CancellablePromise} rejects after being cancelled, * or when the `oncancelled` callback throws or rejects. * * The value of the {@link name} property is the string `"CancelledRejectionError"`. * The value of the {@link cause} property is the reason the promise rejected with. * * Because the original promise was cancelled, * a wrapper promise will be passed to the unhandled rejection listener instead. * The {@link promise} property holds a reference to the original promise. */ export class CancelledRejectionError extends Error { /** * Constructs a new `CancelledRejectionError` instance. * @param promise - The promise that caused the error originally. * @param reason - The rejection reason. * @param info - An optional informative message specifying the circumstances in which the error was thrown. * Defaults to the string `"Unhandled rejection in cancelled promise."`. */ constructor(promise, reason, info) { super((info !== null && info !== void 0 ? info : "Unhandled rejection in cancelled promise.") + " Reason: " + errorMessage(reason), { cause: reason }); this.promise = promise; this.name = "CancelledRejectionError"; } } // Private field names. const barrierSym = Symbol("barrier"); const cancelImplSym = Symbol("cancelImpl"); const species = (_a = Symbol.species) !== null && _a !== void 0 ? _a : Symbol("speciesPolyfill"); /** * A promise with an attached method for cancelling long-running operations (see {@link CancellablePromise#cancel}). * Cancellation can optionally be bound to an {@link AbortSignal} * for better composability (see {@link CancellablePromise#cancelOn}). * * Cancelling a pending promise will result in an immediate rejection * with an instance of {@link CancelError} as reason, * but whoever started the promise will be responsible * for actually aborting the underlying operation. * To this purpose, the constructor and all chaining methods * accept optional cancellation callbacks. * * If a `CancellablePromise` still resolves after having been cancelled, * the result will be discarded. If it rejects, the reason * will be reported as an unhandled rejection, * wrapped in a {@link CancelledRejectionError} instance. * To facilitate the handling of cancellation requests, * cancelled `CancellablePromise`s will _not_ report unhandled `CancelError`s * whose `cause` field is the same as the one with which the current promise was cancelled. * * All usual promise methods are defined and return a `CancellablePromise` * whose cancel method will cancel the parent operation as well, propagating the cancellation reason * upwards through promise chains. * Conversely, cancelling a promise will not automatically cancel dependent promises downstream: * ```ts * let root = new CancellablePromise((resolve, reject) => { ... }); * let child1 = root.then(() => { ... }); * let child2 = child1.then(() => { ... }); * let child3 = root.catch(() => { ... }); * child1.cancel(); // Cancels child1 and root, but not child2 or child3 * ``` * Cancelling a promise that has already settled is safe and has no consequence. * * The `cancel` method returns a promise that _always fulfills_ * after the whole chain has processed the cancel request * and all attached callbacks up to that moment have run. * * All ES2024 promise methods (static and instance) are defined on CancellablePromise, * but actual availability may vary with OS/webview version. * * In line with the proposal at https://github.com/tc39/proposal-rm-builtin-subclassing, * `CancellablePromise` does not support transparent subclassing. * Extenders should take care to provide their own method implementations. * This might be reconsidered in case the proposal is retired. * * CancellablePromise is a wrapper around the DOM Promise object * and is compliant with the [Promises/A+ specification](https://promisesaplus.com/) * (it passes the [compliance suite](https://github.com/promises-aplus/promises-tests)) * if so is the underlying implementation. */ export class CancellablePromise extends Promise { /** * Creates a new `CancellablePromise`. * * @param executor - A callback used to initialize the promise. This callback is passed two arguments: * a `resolve` callback used to resolve the promise with a value * or the result of another promise (possibly cancellable), * and a `reject` callback used to reject the promise with a provided reason or error. * If the value provided to the `resolve` callback is a thenable _and_ cancellable object * (it has a `then` _and_ a `cancel` method), * cancellation requests will be forwarded to that object and the oncancelled will not be invoked anymore. * If any one of the two callbacks is called _after_ the promise has been cancelled, * the provided values will be cancelled and resolved as usual, * but their results will be discarded. * However, if the resolution process ultimately ends up in a rejection * that is not due to cancellation, the rejection reason * will be wrapped in a {@link CancelledRejectionError} * and bubbled up as an unhandled rejection. * @param oncancelled - It is the caller's responsibility to ensure that any operation * started by the executor is properly halted upon cancellation. * This optional callback can be used to that purpose. * It will be called _synchronously_ with a cancellation cause * when cancellation is requested, _after_ the promise has already rejected * with a {@link CancelError}, but _before_ * any {@link then}/{@link catch}/{@link finally} callback runs. * If the callback returns a thenable, the promise returned from {@link cancel} * will only fulfill after the former has settled. * Unhandled exceptions or rejections from the callback will be wrapped * in a {@link CancelledRejectionError} and bubbled up as unhandled rejections. * If the `resolve` callback is called before cancellation with a cancellable promise, * cancellation requests on this promise will be diverted to that promise, * and the original `oncancelled` callback will be discarded. */ constructor(executor, oncancelled) { let resolve; let reject; super((res, rej) => { resolve = res; reject = rej; }); if (this.constructor[species] !== Promise) { throw new TypeError("CancellablePromise does not support transparent subclassing. Please refrain from overriding the [Symbol.species] static property."); } let promise = { promise: this, resolve, reject, get oncancelled() { return oncancelled !== null && oncancelled !== void 0 ? oncancelled : null; }, set oncancelled(cb) { oncancelled = cb !== null && cb !== void 0 ? cb : undefined; } }; const state = { get root() { return state; }, resolving: false, settled: false }; // Setup cancellation system. void Object.defineProperties(this, { [barrierSym]: { configurable: false, enumerable: false, writable: true, value: null }, [cancelImplSym]: { configurable: false, enumerable: false, writable: false, value: cancellerFor(promise, state) } }); // Run the actual executor. const rejector = rejectorFor(promise, state); try { executor(resolverFor(promise, state), rejector); } catch (err) { if (state.resolving) { console.log("Unhandled exception in CancellablePromise executor.", err); } else { rejector(err); } } } /** * Cancels immediately the execution of the operation associated with this promise. * The promise rejects with a {@link CancelError} instance as reason, * with the {@link CancelError#cause} property set to the given argument, if any. * * Has no effect if called after the promise has already settled; * repeated calls in particular are safe, but only the first one * will set the cancellation cause. * * The `CancelError` exception _need not_ be handled explicitly _on the promises that are being cancelled:_ * cancelling a promise with no attached rejection handler does not trigger an unhandled rejection event. * Therefore, the following idioms are all equally correct: * ```ts * new CancellablePromise((resolve, reject) => { ... }).cancel(); * new CancellablePromise((resolve, reject) => { ... }).then(...).cancel(); * new CancellablePromise((resolve, reject) => { ... }).then(...).catch(...).cancel(); * ``` * Whenever some cancelled promise in a chain rejects with a `CancelError` * with the same cancellation cause as itself, the error will be discarded silently. * However, the `CancelError` _will still be delivered_ to all attached rejection handlers * added by {@link then} and related methods: * ```ts * let cancellable = new CancellablePromise((resolve, reject) => { ... }); * cancellable.then(() => { ... }).catch(console.log); * cancellable.cancel(); // A CancelError is printed to the console. * ``` * If the `CancelError` is not handled downstream by the time it reaches * a _non-cancelled_ promise, it _will_ trigger an unhandled rejection event, * just like normal rejections would: * ```ts * let cancellable = new CancellablePromise((resolve, reject) => { ... }); * let chained = cancellable.then(() => { ... }).then(() => { ... }); // No catch... * cancellable.cancel(); // Unhandled rejection event on chained! * ``` * Therefore, it is important to either cancel whole promise chains from their tail, * as shown in the correct idioms above, or take care of handling errors everywhere. * * @returns A cancellable promise that _fulfills_ after the cancel callback (if any) * and all handlers attached up to the call to cancel have run. * If the cancel callback returns a thenable, the promise returned by `cancel` * will also wait for that thenable to settle. * This enables callers to wait for the cancelled operation to terminate * without being forced to handle potential errors at the call site. * ```ts * cancellable.cancel().then(() => { * // Cleanup finished, it's safe to do something else. * }, (err) => { * // Unreachable: the promise returned from cancel will never reject. * }); * ``` * Note that the returned promise will _not_ handle implicitly any rejection * that might have occurred already in the cancelled chain. * It will just track whether registered handlers have been executed or not. * Therefore, unhandled rejections will never be silently handled by calling cancel. */ cancel(cause) { return new CancellablePromise((resolve) => { // INVARIANT: the result of this[cancelImplSym] and the barrier do not ever reject. // Unfortunately macOS High Sierra does not support Promise.allSettled. Promise.all([ this[cancelImplSym](new CancelError("Promise cancelled.", { cause })), currentBarrier(this) ]).then(() => resolve(), () => resolve()); }); } /** * Binds promise cancellation to the abort event of the given {@link AbortSignal}. * If the signal has already aborted, the promise will be cancelled immediately. * When either condition is verified, the cancellation cause will be set * to the signal's abort reason (see {@link AbortSignal#reason}). * * Has no effect if called (or if the signal aborts) _after_ the promise has already settled. * Only the first signal to abort will set the cancellation cause. * * For more details about the cancellation process, * see {@link cancel} and the `CancellablePromise` constructor. * * This method enables `await`ing cancellable promises without having * to store them for future cancellation, e.g.: * ```ts * await longRunningOperation().cancelOn(signal); * ``` * instead of: * ```ts * let promiseToBeCancelled = longRunningOperation(); * await promiseToBeCancelled; * ``` * * @returns This promise, for method chaining. */ cancelOn(signal) { if (signal.aborted) { void this.cancel(signal.reason); } else { signal.addEventListener('abort', () => void this.cancel(signal.reason), { capture: true }); } return this; } /** * Attaches callbacks for the resolution and/or rejection of the `CancellablePromise`. * * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, * with the same semantics as the `oncancelled` argument of the constructor. * When the parent promise rejects or is cancelled, the `onrejected` callback will run, * _even after the returned promise has been cancelled:_ * in that case, should it reject or throw, the reason will be wrapped * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. * * @param onfulfilled The callback to execute when the Promise is resolved. * @param onrejected The callback to execute when the Promise is rejected. * @returns A `CancellablePromise` for the completion of whichever callback is executed. * The returned promise is hooked up to propagate cancellation requests up the chain, but not down: * * - if the parent promise is cancelled, the `onrejected` handler will be invoked with a `CancelError` * and the returned promise _will resolve regularly_ with its result; * - conversely, if the returned promise is cancelled, _the parent promise is cancelled too;_ * the `onrejected` handler will still be invoked with the parent's `CancelError`, * but its result will be discarded * and the returned promise will reject with a `CancelError` as well. * * The promise returned from {@link cancel} will fulfill only after all attached handlers * up the entire promise chain have been run. * * If either callback returns a cancellable promise, * cancellation requests will be diverted to it, * and the specified `oncancelled` callback will be discarded. */ then(onfulfilled, onrejected, oncancelled) { if (!(this instanceof CancellablePromise)) { throw new TypeError("CancellablePromise.prototype.then called on an invalid object."); } // NOTE: TypeScript's built-in type for then is broken, // as it allows specifying an arbitrary TResult1 != T even when onfulfilled is not a function. // We cannot fix it if we want to CancellablePromise to implement PromiseLike. if (!isCallable(onfulfilled)) { onfulfilled = identity; } if (!isCallable(onrejected)) { onrejected = thrower; } if (onfulfilled === identity && onrejected == thrower) { // Shortcut for trivial arguments. return new CancellablePromise((resolve) => resolve(this)); } const barrier = {}; this[barrierSym] = barrier; return new CancellablePromise((resolve, reject) => { void super.then((value) => { var _a; if (this[barrierSym] === barrier) { this[barrierSym] = null; } (_a = barrier.resolve) === null || _a === void 0 ? void 0 : _a.call(barrier); try { resolve(onfulfilled(value)); } catch (err) { reject(err); } }, (reason) => { var _a; if (this[barrierSym] === barrier) { this[barrierSym] = null; } (_a = barrier.resolve) === null || _a === void 0 ? void 0 : _a.call(barrier); try { resolve(onrejected(reason)); } catch (err) { reject(err); } }); }, async (cause) => { //cancelled = true; try { return oncancelled === null || oncancelled === void 0 ? void 0 : oncancelled(cause); } finally { await this.cancel(cause); } }); } /** * Attaches a callback for only the rejection of the Promise. * * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, * with the same semantics as the `oncancelled` argument of the constructor. * When the parent promise rejects or is cancelled, the `onrejected` callback will run, * _even after the returned promise has been cancelled:_ * in that case, should it reject or throw, the reason will be wrapped * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. * * It is equivalent to * ```ts * cancellablePromise.then(undefined, onrejected, oncancelled); * ``` * and the same caveats apply. * * @returns A Promise for the completion of the callback. * Cancellation requests on the returned promise * will propagate up the chain to the parent promise, * but not in the other direction. * * The promise returned from {@link cancel} will fulfill only after all attached handlers * up the entire promise chain have been run. * * If `onrejected` returns a cancellable promise, * cancellation requests will be diverted to it, * and the specified `oncancelled` callback will be discarded. * See {@link then} for more details. */ catch(onrejected, oncancelled) { return this.then(undefined, onrejected, oncancelled); } /** * Attaches a callback that is invoked when the CancellablePromise is settled (fulfilled or rejected). The * resolved value cannot be accessed or modified from the callback. * The returned promise will settle in the same state as the original one * after the provided callback has completed execution, * unless the callback throws or returns a rejecting promise, * in which case the returned promise will reject as well. * * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, * with the same semantics as the `oncancelled` argument of the constructor. * Once the parent promise settles, the `onfinally` callback will run, * _even after the returned promise has been cancelled:_ * in that case, should it reject or throw, the reason will be wrapped * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. * * This method is implemented in terms of {@link then} and the same caveats apply. * It is polyfilled, hence available in every OS/webview version. * * @returns A Promise for the completion of the callback. * Cancellation requests on the returned promise * will propagate up the chain to the parent promise, * but not in the other direction. * * The promise returned from {@link cancel} will fulfill only after all attached handlers * up the entire promise chain have been run. * * If `onfinally` returns a cancellable promise, * cancellation requests will be diverted to it, * and the specified `oncancelled` callback will be discarded. * See {@link then} for more details. */ finally(onfinally, oncancelled) { if (!(this instanceof CancellablePromise)) { throw new TypeError("CancellablePromise.prototype.finally called on an invalid object."); } if (!isCallable(onfinally)) { return this.then(onfinally, onfinally, oncancelled); } return this.then((value) => CancellablePromise.resolve(onfinally()).then(() => value), (reason) => CancellablePromise.resolve(onfinally()).then(() => { throw reason; }), oncancelled); } /** * We use the `[Symbol.species]` static property, if available, * to disable the built-in automatic subclassing features from {@link Promise}. * It is critical for performance reasons that extenders do not override this. * Once the proposal at https://github.com/tc39/proposal-rm-builtin-subclassing * is either accepted or retired, this implementation will have to be revised accordingly. * * @ignore * @internal */ static get [species]() { return Promise; } static all(values) { let collected = Array.from(values); const promise = collected.length === 0 ? CancellablePromise.resolve(collected) : new CancellablePromise((resolve, reject) => { void Promise.all(collected).then(resolve, reject); }, (cause) => cancelAll(promise, collected, cause)); return promise; } static allSettled(values) { let collected = Array.from(values); const promise = collected.length === 0 ? CancellablePromise.resolve(collected) : new CancellablePromise((resolve, reject) => { void Promise.allSettled(collected).then(resolve, reject); }, (cause) => cancelAll(promise, collected, cause)); return promise; } static any(values) { let collected = Array.from(values); const promise = collected.length === 0 ? CancellablePromise.resolve(collected) : new CancellablePromise((resolve, reject) => { void Promise.any(collected).then(resolve, reject); }, (cause) => cancelAll(promise, collected, cause)); return promise; } static race(values) { let collected = Array.from(values); const promise = new CancellablePromise((resolve, reject) => { void Promise.race(collected).then(resolve, reject); }, (cause) => cancelAll(promise, collected, cause)); return promise; } /** * Creates a new cancelled CancellablePromise for the provided cause. * * @group Static Methods */ static cancel(cause) { const p = new CancellablePromise(() => { }); p.cancel(cause); return p; } /** * Creates a new CancellablePromise that cancels * after the specified timeout, with the provided cause. * * If the {@link AbortSignal.timeout} factory method is available, * it is used to base the timeout on _active_ time rather than _elapsed_ time. * Otherwise, `timeout` falls back to {@link setTimeout}. * * @group Static Methods */ static timeout(milliseconds, cause) { const promise = new CancellablePromise(() => { }); if (AbortSignal && typeof AbortSignal === 'function' && AbortSignal.timeout && typeof AbortSignal.timeout === 'function') { AbortSignal.timeout(milliseconds).addEventListener('abort', () => void promise.cancel(cause)); } else { setTimeout(() => void promise.cancel(cause), milliseconds); } return promise; } static sleep(milliseconds, value) { return new CancellablePromise((resolve) => { setTimeout(() => resolve(value), milliseconds); }); } /** * Creates a new rejected CancellablePromise for the provided reason. * * @group Static Methods */ static reject(reason) { return new CancellablePromise((_, reject) => reject(reason)); } static resolve(value) { if (value instanceof CancellablePromise) { // Optimise for cancellable promises. return value; } return new CancellablePromise((resolve) => resolve(value)); } /** * Creates a new CancellablePromise and returns it in an object, along with its resolve and reject functions * and a getter/setter for the cancellation callback. * * This method is polyfilled, hence available in every OS/webview version. * * @group Static Methods */ static withResolvers() { let result = { oncancelled: null }; result.promise = new CancellablePromise((resolve, reject) => { result.resolve = resolve; result.reject = reject; }, (cause) => { var _a; (_a = result.oncancelled) === null || _a === void 0 ? void 0 : _a.call(result, cause); }); return result; } } /** * Returns a callback that implements the cancellation algorithm for the given cancellable promise. * The promise returned from the resulting function does not reject. */ function cancellerFor(promise, state) { let cancellationPromise = undefined; return (reason) => { if (!state.settled) { state.settled = true; state.reason = reason; promise.reject(reason); // Attach an error handler that ignores this specific rejection reason and nothing else. // In theory, a sane underlying implementation at this point // should always reject with our cancellation reason, // hence the handler will never throw. void Promise.prototype.then.call(promise.promise, undefined, (err) => { if (err !== reason) { throw err; } }); } // If reason is not set, the promise resolved regularly, hence we must not call oncancelled. // If oncancelled is unset, no need to go any further. if (!state.reason || !promise.oncancelled) { return; } cancellationPromise = new Promise((resolve) => { try { resolve(promise.oncancelled(state.reason.cause)); } catch (err) { Promise.reject(new CancelledRejectionError(promise.promise, err, "Unhandled exception in oncancelled callback.")); } }).catch((reason) => { Promise.reject(new CancelledRejectionError(promise.promise, reason, "Unhandled rejection in oncancelled callback.")); }); // Unset oncancelled to prevent repeated calls. promise.oncancelled = null; return cancellationPromise; }; } /** * Returns a callback that implements the resolution algorithm for the given cancellable promise. */ function resolverFor(promise, state) { return (value) => { if (state.resolving) { return; } state.resolving = true; if (value === promise.promise) { if (state.settled) { return; } state.settled = true; promise.reject(new TypeError("A promise cannot be resolved with itself.")); return; } if (value != null && (typeof value === 'object' || typeof value === 'function')) { let then; try { then = value.then; } catch (err) { state.settled = true; promise.reject(err); return; } if (isCallable(then)) { try { let cancel = value.cancel; if (isCallable(cancel)) { const oncancelled = (cause) => { Reflect.apply(cancel, value, [cause]); }; if (state.reason) { // If already cancelled, propagate cancellation. // The promise returned from the canceller algorithm does not reject // so it can be discarded safely. void cancellerFor(Object.assign(Object.assign({}, promise), { oncancelled }), state)(state.reason); } else { promise.oncancelled = oncancelled; } } } catch (_a) { } const newState = { root: state.root, resolving: false, get settled() { return this.root.settled; }, set settled(value) { this.root.settled = value; }, get reason() { return this.root.reason; } }; const rejector = rejectorFor(promise, newState); try { Reflect.apply(then, value, [resolverFor(promise, newState), rejector]); } catch (err) { rejector(err); } return; // IMPORTANT! } } if (state.settled) { return; } state.settled = true; promise.resolve(value); }; } /** * Returns a callback that implements the rejection algorithm for the given cancellable promise. */ function rejectorFor(promise, state) { return (reason) => { if (state.resolving) { return; } state.resolving = true; if (state.settled) { try { if (reason instanceof CancelError && state.reason instanceof CancelError && Object.is(reason.cause, state.reason.cause)) { // Swallow late rejections that are CancelErrors whose cancellation cause is the same as ours. return; } } catch (_a) { } void Promise.reject(new CancelledRejectionError(promise.promise, reason)); } else { state.settled = true; promise.reject(reason); } }; } /** * Cancels all values in an array that look like cancellable thenables. * Returns a promise that fulfills once all cancellation procedures for the given values have settled. */ function cancelAll(parent, values, cause) { const results = []; for (const value of values) { let cancel; try { if (!isCallable(value.then)) { continue; } cancel = value.cancel; if (!isCallable(cancel)) { continue; } } catch (_a) { continue; } let result; try { result = Reflect.apply(cancel, value, [cause]); } catch (err) { Promise.reject(new CancelledRejectionError(parent, err, "Unhandled exception in cancel method.")); continue; } if (!result) { continue; } results.push((result instanceof Promise ? result : Promise.resolve(result)).catch((reason) => { Promise.reject(new CancelledRejectionError(parent, reason, "Unhandled rejection in cancel method.")); })); } return Promise.all(results); } /** * Returns its argument. */ function identity(x) { return x; } /** * Throws its argument. */ function thrower(reason) { throw reason; } /** * Attempts various strategies to convert an error to a string. */ function errorMessage(err) { try { if (err instanceof Error || typeof err !== 'object' || err.toString !== Object.prototype.toString) { return "" + err; } } catch (_a) { } try { return JSON.stringify(err); } catch (_b) { } try { return Object.prototype.toString.call(err); } catch (_c) { } return ""; } /** * Gets the current barrier promise for the given cancellable promise. If necessary, initialises the barrier. */ function currentBarrier(promise) { var _a; let pwr = (_a = promise[barrierSym]) !== null && _a !== void 0 ? _a : {}; if (!('promise' in pwr)) { Object.assign(pwr, promiseWithResolvers()); } if (promise[barrierSym] == null) { pwr.resolve(); promise[barrierSym] = pwr; } return pwr.promise; } // Polyfill Promise.withResolvers. let promiseWithResolvers = Promise.withResolvers; if (promiseWithResolvers && typeof promiseWithResolvers === 'function') { promiseWithResolvers = promiseWithResolvers.bind(Promise); } else { promiseWithResolvers = function () { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; }; }