diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.test.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.test.js index 4a86a2abe..b618c1459 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.test.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.test.js @@ -12,6 +12,7 @@ import { CancelError, CancellablePromise, CancelledRejectionError } from "./canc // it should be reported as an unhandled rejection, // - unless it is a CancelError with the same reason given for cancelling the returned promise. // TODO: test multiple calls to cancel() (second and later should have no effect). +// TODO: test static factory methods and their cancellation support. let expectedUnhandled = new Map(); diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts index 8406b78b3..acc8236aa 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts @@ -68,7 +68,7 @@ type CancellablePromiseExecutor = (resolve: CancellablePromiseResolver, re export interface CancellablePromiseLike { then(onfulfilled?: ((value: T) => TResult1 | PromiseLike | CancellablePromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike | CancellablePromiseLike) | undefined | null): CancellablePromiseLike; - cancel(cause?: any): void; + cancel(cause?: any): void | PromiseLike; } /** @@ -292,7 +292,9 @@ export class CancellablePromise extends Promise implements PromiseLike, */ cancel(cause?: any): CancellablePromise { return new CancellablePromise((resolve) => { - Promise.allSettled([ + // 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()); @@ -523,11 +525,12 @@ export class CancellablePromise extends Promise implements PromiseLike, static all(values: T): CancellablePromise<{ -readonly [P in keyof T]: Awaited; }>; static all | ArrayLike>(values: T): CancellablePromise { let collected = Array.from(values); - return collected.length === 0 + const promise = collected.length === 0 ? CancellablePromise.resolve(collected) : new CancellablePromise((resolve, reject) => { void Promise.all(collected).then(resolve, reject); - }, allCanceller(collected)); + }, (cause?): Promise => cancelAll(promise, collected, cause)); + return promise; } /** @@ -543,11 +546,12 @@ export class CancellablePromise extends Promise implements PromiseLike, static allSettled(values: T): CancellablePromise<{ -readonly [P in keyof T]: PromiseSettledResult>; }>; static allSettled | ArrayLike>(values: T): CancellablePromise { let collected = Array.from(values); - return collected.length === 0 + const promise = collected.length === 0 ? CancellablePromise.resolve(collected) : new CancellablePromise((resolve, reject) => { void Promise.allSettled(collected).then(resolve, reject); - }, allCanceller(collected)); + }, (cause?): Promise => cancelAll(promise, collected, cause)); + return promise; } /** @@ -565,11 +569,12 @@ export class CancellablePromise extends Promise implements PromiseLike, static any(values: T): CancellablePromise>; static any | ArrayLike>(values: T): CancellablePromise { let collected = Array.from(values); - return collected.length === 0 + const promise = collected.length === 0 ? CancellablePromise.resolve(collected) : new CancellablePromise((resolve, reject) => { void Promise.any(collected).then(resolve, reject); - }, allCanceller(collected)); + }, (cause?): Promise => cancelAll(promise, collected, cause)); + return promise; } /** @@ -584,9 +589,10 @@ export class CancellablePromise extends Promise implements PromiseLike, static race(values: T): CancellablePromise>; static race | ArrayLike>(values: T): CancellablePromise { let collected = Array.from(values); - return new CancellablePromise((resolve, reject) => { + const promise = new CancellablePromise((resolve, reject) => { void Promise.race(collected).then(resolve, reject); - }, allCanceller(collected)); + }, (cause?): Promise => cancelAll(promise, collected, cause)); + return promise; } /** @@ -722,15 +728,14 @@ function cancellerFor(promise: CancellablePromiseWithResolvers, state: Can // If oncancelled is unset, no need to go any further. if (!state.reason || !promise.oncancelled) { return; } - cancellationPromise = new Promise((resolve) => { + 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.")); } - }); - cancellationPromise.then(undefined, (reason?) => { - throw new CancelledRejectionError(promise.promise, reason, "Unhandled rejection in oncancelled callback."); + }).catch((reason?) => { + Promise.reject(new CancelledRejectionError(promise.promise, reason, "Unhandled rejection in oncancelled callback.")); }); // Unset oncancelled to prevent repeated calls. @@ -832,21 +837,37 @@ function rejectorFor(promise: CancellablePromiseWithResolvers, state: Canc } /** - * Returns a callback that cancels all values in an iterable that look like cancellable thenables. + * 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 allCanceller(values: Iterable): CancellablePromiseCanceller { - return (cause?) => { - for (const value of values) { - try { - if (isCallable(value.then)) { - let cancel = value.cancel; - if (isCallable(cancel)) { - Reflect.apply(cancel, value, [cause]); - } - } - } catch {} +function cancelAll(parent: CancellablePromise, values: any[], cause?: any): Promise { + const results = []; + + for (const value of values) { + let cancel: CancellablePromiseCanceller; + try { + if (!isCallable(value.then)) { continue; } + cancel = value.cancel; + if (!isCallable(cancel)) { continue; } + } catch { continue; } + + let result: void | PromiseLike; + 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) as any; } /** diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts index 7905ead84..09d0dc243 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts @@ -45,8 +45,8 @@ export { /** * An internal utility consumed by the binding generator. * - * @private * @ignore + * @internal */ export { Create };