( validateAsync: (values: T) => Promise<ValidationResult<T>>, debounceMs: number, onResult: (errors: ValidationResult<T>) => void, onError?: (error: unknown) => void, )
| 104 | * @returns Object with run and cancel methods |
| 105 | */ |
| 106 | export function createDebouncedAsyncValidator<T extends Record<string, unknown>>( |
| 107 | validateAsync: (values: T) => Promise<ValidationResult<T>>, |
| 108 | debounceMs: number, |
| 109 | onResult: (errors: ValidationResult<T>) => void, |
| 110 | onError?: (error: unknown) => void, |
| 111 | ): Readonly<{ |
| 112 | run: (values: T) => void; |
| 113 | cancel: () => void; |
| 114 | }> { |
| 115 | let timeoutId: ReturnType<typeof setTimeout> | undefined; |
| 116 | let cancelled = false; |
| 117 | // Monotonic token used to ignore stale debounced callbacks and in-flight promises. |
| 118 | let token = 0; |
| 119 | |
| 120 | return Object.freeze({ |
| 121 | run(values: T): void { |
| 122 | // Cancel any pending validation |
| 123 | if (timeoutId !== undefined) { |
| 124 | clearTimeout(timeoutId); |
| 125 | } |
| 126 | cancelled = false; |
| 127 | token++; |
| 128 | const myToken = token; |
| 129 | |
| 130 | timeoutId = setTimeout(() => { |
| 131 | if (cancelled || myToken !== token) return; |
| 132 | |
| 133 | validateAsync(values) |
| 134 | .then((errors) => { |
| 135 | if (!cancelled && myToken === token) { |
| 136 | onResult(errors); |
| 137 | } |
| 138 | }) |
| 139 | .catch((e) => { |
| 140 | if (cancelled || myToken !== token) return; |
| 141 | if (onError) { |
| 142 | onError(e); |
| 143 | } else { |
| 144 | onResult({}); |
| 145 | } |
| 146 | }); |
| 147 | }, debounceMs); |
| 148 | }, |
| 149 | |
| 150 | cancel(): void { |
| 151 | cancelled = true; |
| 152 | token++; |
| 153 | if (timeoutId !== undefined) { |
| 154 | clearTimeout(timeoutId); |
| 155 | timeoutId = undefined; |
| 156 | } |
| 157 | }, |
| 158 | }); |
| 159 | } |
| 160 | |
| 161 | /** |
| 162 | * Run async validation immediately (without debounce). |
no outgoing calls
no test coverage detected