# HG changeset patch # User cin # Date 2021-05-18 23:07:51 # Node ID 1391543e82829382dfdcc8849b0f1184125e54bc # Parent 204c5274031f1682eef21355e7ba2997e211f259 Added Cancellation.combine(...tokens) method to combine multiple cancellation tokens to the aggregated token. diff --git a/src/main/ts/Cancellation.ts b/src/main/ts/Cancellation.ts --- a/src/main/ts/Cancellation.ts +++ b/src/main/ts/Cancellation.ts @@ -1,3 +1,4 @@ +import { CancellationAggregate } from "./CancellationAggregate"; import { ICancellation, IDestroyable } from "./interfaces"; import { argumentNotNull, destroyed } from "./safe"; @@ -80,4 +81,26 @@ export class Cancellation implements ICa return destroyed; } }; + + /** + * Combines multiple cancellation tokens to the single aggregated token. + * + * Aggregated token will be considered as signalled when some tokens are + * signalled. The cancellation callback can be registered with the `register` + * method, it will be fired once with the first signalled token, all other + * tokens will be ignored. + * + * The tokens which don't support cancellation are filtered out, if there are + * no tokens left in the list the method returns `Cancellation.none`. + * + * @param args The list of cancellation tokens to combine + * @returns + */ + static combine(...args: ICancellation[]) { + const tokens = args.filter(ct => ct.isSupported()); + return tokens.length > 1 ? + new CancellationAggregate(tokens) : + tokens.length == 1 ? tokens[0] : + this.none; + } } diff --git a/src/main/ts/CancellationAggregate.ts b/src/main/ts/CancellationAggregate.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/CancellationAggregate.ts @@ -0,0 +1,41 @@ +import { ICancellation, IDestroyable } from "./interfaces"; + +export class CancellationAggregate implements ICancellation { + private readonly _tokens: ICancellation[]; + + constructor(tokens: ICancellation[]) { + this._tokens = tokens || []; + } + + throwIfRequested() { + this._tokens.forEach(ct => ct.throwIfRequested()); + } + + isRequested() { + return this._tokens.some(ct => ct.isRequested()); + } + isSupported() { + return !!this._tokens.length; + } + register(cb: (e: any) => void): IDestroyable { + let fired = false; + + const once = (e: any) => { + if (!fired) { + fired = true; + destroy(); + cb(e); + } + } + + const destroy = () => subscriptions + .splice(0,subscriptions.length) // empty array + .forEach(subscription => subscription.destroy()); // cleanup + + const subscriptions = this._tokens.map(ct => ct.register(once)) + + return { + destroy + }; + } +} diff --git a/src/main/ts/components/AsyncComponent.ts b/src/main/ts/components/AsyncComponent.ts --- a/src/main/ts/components/AsyncComponent.ts +++ b/src/main/ts/components/AsyncComponent.ts @@ -1,6 +1,5 @@ import { Cancellation } from "../Cancellation"; -import { IAsyncComponent, ICancellation, ICancellable, IDestroyable } from "../interfaces"; -import { destroy } from "../safe"; +import { IAsyncComponent, ICancellation, ICancellable } from "../interfaces"; const noop = () => void (0); @@ -13,21 +12,16 @@ export class AsyncComponent implements I runOperation(op: (ct: ICancellation) => any, ct: ICancellation = Cancellation.none) { // create inner cancellation bound to the passed cancellation token - let h: IDestroyable; const inner = new Cancellation(cancel => { - this._cancel = cancel; - h = ct.register(cancel); }); - // TODO create cancellation source here const guard = async () => { try { - await op(inner); + return op(Cancellation.combine(ct, inner)); } finally { // after the operation is complete we need to cleanup the // resources - destroy(h); this._cancel = noop; } }; diff --git a/src/test/ts/tests/CancellationTests.ts b/src/test/ts/tests/CancellationTests.ts --- a/src/test/ts/tests/CancellationTests.ts +++ b/src/test/ts/tests/CancellationTests.ts @@ -1,4 +1,5 @@ import { Cancellation } from "../Cancellation"; +import { CancellationAggregate } from "../CancellationAggregate"; import { delay, notImplemented } from "../safe"; import { test } from "./TestTraits"; @@ -86,3 +87,20 @@ test("operation normal flow", async t => clearTimeout(htimeout); } }); + +test("combine cancellations", t => { + const ct1 = new Cancellation(cancel => { + cancel("cancelled"); + }); + + const ct2 = new Cancellation(() => {}); + + const cct1 = Cancellation.combine(Cancellation.none, ct1); + + t.equals(cct1, ct1, "Cancellation.combine should filter out Cancellation.none tokens"); + + const cct2 = Cancellation.combine(Cancellation.none, ct1, ct2); + + t.assert(cct2 instanceof CancellationAggregate, "Cancellation.combine should return CancellationAggregate"); + t.true(cct2.isRequested(), "CancellationAggregate should return isRequested true if any of cancellations is requested"); +});