diff --git a/.vscode/settings.json b/.vscode/settings.json index e628b9d..8113e2a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,5 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.organizeImports": true - } + }, } diff --git a/changelog.md b/changelog.md index 023366b..7beb108 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog +## 3.1.0 + +- **feat:** add new option `abortOnReturn` to timeouts ([#72](https://github.com/connor4312/cockatiel/issues/72)) + ## 3.0.0 - **breaking:** please see the breaking changes for the two 3.0.0-beta releases diff --git a/src/Policy.ts b/src/Policy.ts index 1db7bb8..6404d59 100644 --- a/src/Policy.ts +++ b/src/Policy.ts @@ -311,8 +311,14 @@ export function usePolicy(policy: IPolicy) { * {@link TaskCancelledError} when the timeout is reached, in addition to * marking the passed token as failed. */ -export function timeout(duration: number, strategy: TimeoutStrategy) { - return new TimeoutPolicy(duration, strategy); +export function timeout( + duration: number, + strategyOrOpts: TimeoutStrategy | { strategy: TimeoutStrategy; abortOnReturn: boolean }, +) { + return new TimeoutPolicy( + duration, + typeof strategyOrOpts === 'string' ? { strategy: strategyOrOpts } : strategyOrOpts, + ); } /** diff --git a/src/TimeoutPolicy.test.ts b/src/TimeoutPolicy.test.ts index 90f2861..575c45e 100644 --- a/src/TimeoutPolicy.test.ts +++ b/src/TimeoutPolicy.test.ts @@ -83,6 +83,24 @@ describe('TimeoutPolicy', () => { }, parent.signal); }); + it('aborts on return by default', async () => { + let signal: AbortSignal; + await timeout(1, TimeoutStrategy.Cooperative).execute(async (_, s) => { + signal = s; + }); + expect(signal!.aborted).to.be.true; + }); + + it('does not aborts on return if requested', async () => { + let signal: AbortSignal; + await timeout(1, { strategy: TimeoutStrategy.Aggressive, abortOnReturn: false }).execute( + async (_, s) => { + signal = s; + }, + ); + expect(signal!.aborted).to.be.false; + }); + describe('events', () => { let onSuccess: SinonStub; let onFailure: SinonStub; diff --git a/src/TimeoutPolicy.ts b/src/TimeoutPolicy.ts index 51ea10a..6c0d249 100644 --- a/src/TimeoutPolicy.ts +++ b/src/TimeoutPolicy.ts @@ -21,6 +21,16 @@ export interface ICancellationContext { signal: AbortSignal; } +export interface ITimeoutOptions { + /** Strategy for timeouts, "Cooperative", or "Accessive" */ + strategy: TimeoutStrategy; + /** + * Whether the AbortSignal should be aborted when the + * function returns. Defaults to true. + */ + abortOnReturn?: boolean; +} + export class TimeoutPolicy implements IPolicy { declare readonly _altReturn: never; @@ -43,7 +53,7 @@ export class TimeoutPolicy implements IPolicy { constructor( private readonly duration: number, - private readonly strategy: TimeoutStrategy, + private readonly options: ITimeoutOptions, private readonly executor = new ExecuteWrapper(), private readonly unref = false, ) {} @@ -56,7 +66,7 @@ export class TimeoutPolicy implements IPolicy { * timeout might still be happening. */ public dangerouslyUnref() { - const t = new TimeoutPolicy(this.duration, this.strategy, this.executor, true); + const t = new TimeoutPolicy(this.duration, this.options, this.executor, true); return t; } @@ -81,7 +91,7 @@ export class TimeoutPolicy implements IPolicy { const onCancelledListener = onceAborted(() => this.timeoutEmitter.emit()); try { - if (this.strategy === TimeoutStrategy.Cooperative) { + if (this.options.strategy === TimeoutStrategy.Cooperative) { return returnOrThrow(await this.executor.invoke(fn, context, aborter.signal)); } @@ -97,7 +107,9 @@ export class TimeoutPolicy implements IPolicy { .then(returnOrThrow); } finally { onCancelledListener.dispose(); - aborter.abort(); + if (this.options.abortOnReturn !== false) { + aborter.abort(); + } clearTimeout(timer); } }