diff --git a/library/src/actions/check/checkAsync.ts b/library/src/actions/check/checkAsync.ts index 68c109209..4f457516a 100644 --- a/library/src/actions/check/checkAsync.ts +++ b/library/src/actions/check/checkAsync.ts @@ -28,7 +28,10 @@ export interface CheckActionAsync< /** * The validation function. */ - readonly requirement: (input: TInput) => MaybePromise; + readonly requirement: ( + input: TInput, + signal?: AbortSignal + ) => MaybePromise; /** * The error message. */ @@ -43,7 +46,7 @@ export interface CheckActionAsync< * @returns A check action. */ export function checkAsync( - requirement: (input: TInput) => MaybePromise + requirement: (input: TInput, signal?: AbortSignal) => MaybePromise ): CheckActionAsync; /** @@ -58,13 +61,13 @@ export function checkAsync< TInput, const TMessage extends ErrorMessage> | undefined, >( - requirement: (input: TInput) => MaybePromise, + requirement: (input: TInput, signal?: AbortSignal) => MaybePromise, message: TMessage ): CheckActionAsync; // @__NO_SIDE_EFFECTS__ export function checkAsync( - requirement: (input: unknown) => MaybePromise, + requirement: (input: unknown, signal?: AbortSignal) => MaybePromise, message?: ErrorMessage> ): CheckActionAsync> | undefined> { return { @@ -76,7 +79,10 @@ export function checkAsync( requirement, message, async '~run'(dataset, config) { - if (dataset.typed && !(await this.requirement(dataset.value))) { + if ( + dataset.typed && + !(await this.requirement(dataset.value, config.signal)) + ) { _addIssue(this, 'input', dataset, config); } return dataset; diff --git a/library/src/actions/checkItems/checkItemsAsync.ts b/library/src/actions/checkItems/checkItemsAsync.ts index 98c68f497..6de703fd5 100644 --- a/library/src/actions/checkItems/checkItemsAsync.ts +++ b/library/src/actions/checkItems/checkItemsAsync.ts @@ -78,7 +78,9 @@ export function checkItemsAsync( async '~run'(dataset, config) { if (dataset.typed) { const requirementResults = await Promise.all( - dataset.value.map(this.requirement) + dataset.value.map((...args) => + this.requirement(...args, config.signal) + ) ); for (let index = 0; index < dataset.value.length; index++) { if (!requirementResults[index]) { diff --git a/library/src/actions/partialCheck/partialCheckAsync.ts b/library/src/actions/partialCheck/partialCheckAsync.ts index f6b3a1a5a..04b167e06 100644 --- a/library/src/actions/partialCheck/partialCheckAsync.ts +++ b/library/src/actions/partialCheck/partialCheckAsync.ts @@ -42,7 +42,10 @@ export interface PartialCheckActionAsync< /** * The validation function. */ - readonly requirement: (input: TSelection) => MaybePromise; + readonly requirement: ( + input: TSelection, + signal?: AbortSignal + ) => MaybePromise; /** * The error message. */ @@ -63,7 +66,10 @@ export function partialCheckAsync< const TSelection extends DeepPickN, >( paths: ValidPaths, - requirement: (input: TSelection) => MaybePromise + requirement: ( + input: TSelection, + signal?: AbortSignal + ) => MaybePromise ): PartialCheckActionAsync; /** @@ -84,14 +90,20 @@ export function partialCheckAsync< | undefined, >( paths: ValidPaths, - requirement: (input: TSelection) => MaybePromise, + requirement: ( + input: TSelection, + signal?: AbortSignal + ) => MaybePromise, message: TMessage ): PartialCheckActionAsync; // @__NO_SIDE_EFFECTS__ export function partialCheckAsync( paths: Paths, - requirement: (input: PartialInput) => MaybePromise, + requirement: ( + input: PartialInput, + signal?: AbortSignal + ) => MaybePromise, message?: ErrorMessage> ): PartialCheckActionAsync< PartialInput, @@ -112,7 +124,7 @@ export function partialCheckAsync( if ( (dataset.typed || _isPartiallyTyped(dataset, paths)) && // @ts-expect-error - !(await this.requirement(dataset.value)) + !(await this.requirement(dataset.value, config.signal)) ) { _addIssue(this, 'input', dataset, config); } diff --git a/library/src/actions/transform/transformAsync.ts b/library/src/actions/transform/transformAsync.ts index 8fe6588a8..0e5a208ee 100644 --- a/library/src/actions/transform/transformAsync.ts +++ b/library/src/actions/transform/transformAsync.ts @@ -19,7 +19,7 @@ export interface TransformActionAsync /** * The transformation operation. */ - readonly operation: (input: TInput) => Promise; + readonly operation: (input: TInput, signal?: AbortSignal) => Promise; } /** @@ -31,7 +31,7 @@ export interface TransformActionAsync */ // @__NO_SIDE_EFFECTS__ export function transformAsync( - operation: (input: TInput) => Promise + operation: (input: TInput, signal?: AbortSignal) => Promise ): TransformActionAsync { return { kind: 'transformation', @@ -39,9 +39,9 @@ export function transformAsync( reference: transformAsync, async: true, operation, - async '~run'(dataset) { + async '~run'(dataset, config) { // @ts-expect-error - dataset.value = await this.operation(dataset.value); + dataset.value = await this.operation(dataset.value, config.signal); // @ts-expect-error return dataset as SuccessDataset; }, diff --git a/library/src/actions/types.ts b/library/src/actions/types.ts index a2e207c71..30ebdbe96 100644 --- a/library/src/actions/types.ts +++ b/library/src/actions/types.ts @@ -20,7 +20,8 @@ export type ArrayRequirement = ( export type ArrayRequirementAsync = ( item: TInput[number], index: number, - array: TInput + array: TInput, + signal?: AbortSignal ) => MaybePromise; /** diff --git a/library/src/methods/parse/parseAsync.test.ts b/library/src/methods/parse/parseAsync.test.ts index c0a9cf3e1..88b0ece45 100644 --- a/library/src/methods/parse/parseAsync.test.ts +++ b/library/src/methods/parse/parseAsync.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from 'vitest'; -import { transform } from '../../actions/index.ts'; +import { checkAsync, transform } from '../../actions/index.ts'; import { number, objectAsync, string } from '../../schemas/index.ts'; -import { pipe } from '../pipe/index.ts'; +import { pipe, pipeAsync } from '../pipe/index.ts'; import { parseAsync } from './parseAsync.ts'; describe('parseAsync', () => { @@ -29,4 +29,26 @@ describe('parseAsync', () => { parseAsync(objectAsync(entries), null) ).rejects.toThrowError(); }); + + describe('abortSignal', () => { + test('should abort', async () => { + const abort = new AbortController(); + const promise = expect(() => + parseAsync( + pipeAsync( + string(), + checkAsync(async (_, signal) => { + await 0; + signal?.throwIfAborted(); + return true; + }) + ), + 'foo', + { signal: abort.signal } + ) + ).rejects.toThrowError('abort'); + abort.abort('abort'); + await promise; + }); + }); }); diff --git a/library/src/methods/pipe/pipeAsync.test.ts b/library/src/methods/pipe/pipeAsync.test.ts index cda5e53f6..2e8e7d8fd 100644 --- a/library/src/methods/pipe/pipeAsync.test.ts +++ b/library/src/methods/pipe/pipeAsync.test.ts @@ -154,4 +154,13 @@ describe('pipeAsync', () => { }); }); }); + + describe('abortSignal', () => { + test('should abort', async () => { + const signal = AbortSignal.abort('abort'); + await expect(() => + schema['~run']({ value: 'foo' }, { signal }) + ).rejects.toThrowError('abort'); + }); + }); }); diff --git a/library/src/methods/pipe/pipeAsync.ts b/library/src/methods/pipe/pipeAsync.ts index 3db28ae2f..61ab6a797 100644 --- a/library/src/methods/pipe/pipeAsync.ts +++ b/library/src/methods/pipe/pipeAsync.ts @@ -3104,6 +3104,7 @@ export function pipeAsync< } // Continue pipe execution if there is no reason to abort early + config.signal?.throwIfAborted(); if ( !dataset.issues || (!config.abortEarly && !config.abortPipeEarly) diff --git a/library/src/schemas/custom/customAsync.ts b/library/src/schemas/custom/customAsync.ts index 1d880e2bb..b8085f48a 100644 --- a/library/src/schemas/custom/customAsync.ts +++ b/library/src/schemas/custom/customAsync.ts @@ -11,7 +11,10 @@ import type { CustomIssue } from './types.ts'; /** * Check async type. */ -type CheckAsync = (input: unknown) => MaybePromise; +type CheckAsync = ( + input: unknown, + signal?: AbortSignal +) => MaybePromise; /** * Custom schema async interface. @@ -85,7 +88,7 @@ export function customAsync( return _getStandardProps(this); }, async '~run'(dataset, config) { - if (await this.check(dataset.value)) { + if (await this.check(dataset.value, config.signal)) { // @ts-expect-error dataset.typed = true; } else { diff --git a/library/src/schemas/lazy/lazyAsync.test.ts b/library/src/schemas/lazy/lazyAsync.test.ts index 3d3e63de8..9e20fa5ac 100644 --- a/library/src/schemas/lazy/lazyAsync.test.ts +++ b/library/src/schemas/lazy/lazyAsync.test.ts @@ -99,6 +99,14 @@ describe('lazyAsync', () => { const getter = vi.fn(() => string()); const dataset = { value: 'foo' }; lazyAsync(getter)['~run'](dataset, {}); - expect(getter).toHaveBeenCalledWith(dataset.value); + expect(getter).toHaveBeenCalledWith(dataset.value, undefined); + }); + + test('should call getter with signal', () => { + const getter = vi.fn(() => string()); + const dataset = { value: 'foo' }; + const signal = AbortSignal.abort(); + lazyAsync(getter)['~run'](dataset, { signal }); + expect(getter).toHaveBeenCalledWith(dataset.value, signal); }); }); diff --git a/library/src/schemas/lazy/lazyAsync.ts b/library/src/schemas/lazy/lazyAsync.ts index 5955802eb..178a26401 100644 --- a/library/src/schemas/lazy/lazyAsync.ts +++ b/library/src/schemas/lazy/lazyAsync.ts @@ -37,7 +37,10 @@ export interface LazySchemaAsync< /** * The schema getter. */ - readonly getter: (input: unknown) => MaybePromise; + readonly getter: ( + input: unknown, + signal?: AbortSignal + ) => MaybePromise; } /** @@ -53,7 +56,7 @@ export function lazyAsync< | BaseSchema> | BaseSchemaAsync>, >( - getter: (input: unknown) => MaybePromise + getter: (input: unknown, signal?: AbortSignal) => MaybePromise ): LazySchemaAsync { return { kind: 'schema', @@ -66,7 +69,10 @@ export function lazyAsync< return _getStandardProps(this); }, async '~run'(dataset, config) { - return (await this.getter(dataset.value))['~run'](dataset, config); + return (await this.getter(dataset.value, config.signal))['~run']( + dataset, + config + ); }, }; } diff --git a/library/src/storages/globalConfig/globalConfig.test.ts b/library/src/storages/globalConfig/globalConfig.test.ts index ead98cb85..ce8788939 100644 --- a/library/src/storages/globalConfig/globalConfig.test.ts +++ b/library/src/storages/globalConfig/globalConfig.test.ts @@ -13,6 +13,7 @@ describe('config', () => { message: undefined, abortEarly: undefined, abortPipeEarly: undefined, + signal: undefined, }; const customConfig: GlobalConfig = { diff --git a/library/src/storages/globalConfig/globalConfig.ts b/library/src/storages/globalConfig/globalConfig.ts index 5bb03db26..dfbacb92d 100644 --- a/library/src/storages/globalConfig/globalConfig.ts +++ b/library/src/storages/globalConfig/globalConfig.ts @@ -35,6 +35,7 @@ export function getGlobalConfig>( message: config?.message, abortEarly: config?.abortEarly ?? store?.abortEarly, abortPipeEarly: config?.abortPipeEarly ?? store?.abortPipeEarly, + signal: config?.signal, }; } diff --git a/library/src/types/config.ts b/library/src/types/config.ts index d525e2751..45b1839f6 100644 --- a/library/src/types/config.ts +++ b/library/src/types/config.ts @@ -21,4 +21,10 @@ export interface Config> { * Whether a pipe should be aborted early. */ readonly abortPipeEarly?: boolean | undefined; + /** + * The abort signal. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal + */ + readonly signal?: AbortSignal | undefined; }