@@ -3,40 +3,6 @@ import { pDefer } from '../promise/pDefer.js'
33import type { DeferredPromise } from '../promise/pDefer.js'
44import type { AnyAsyncFunction , AnyFunction } from '../types.js'
55
6- export interface Cancelable {
7- cancel : ( ) => void
8- flush : ( ) => void
9- }
10-
11- export interface ThrottleOptions {
12- /**
13- * @default true
14- */
15- leading ?: boolean
16-
17- /**
18- * @default true
19- */
20- trailing ?: boolean
21- }
22-
23- export interface DebounceOptions {
24- /**
25- * @default false
26- */
27- leading ?: boolean
28-
29- /**
30- * @default true
31- */
32- trailing ?: boolean
33-
34- /**
35- *
36- */
37- maxWait ?: number
38- }
39-
406export function _debounce < T extends AnyFunction > (
417 func : T ,
428 wait : number ,
@@ -161,25 +127,21 @@ export function _throttle<T extends AnyFunction>(
161127/**
162128 * Like `_debounce`, but for async functions.
163129 *
164- * Unlike `_debounce` (which returns the previous invocation's stale `result` or `undefined`
165- * for suppressed calls), `_asyncDebounce` always returns a real Promise. All calls that are
166- * coalesced into a single invocation resolve (or reject) with that invocation's result.
167- *
168- * `cancel()` rejects any pending Promises, so awaiters never hang.
130+ * Unlike `_debounce` (which returns a stale `result`/`undefined` for suppressed calls),
131+ * `_asyncDebounce` always returns a real Promise, resolving with the coalesced invocation's result.
169132 *
170133 * @experimental
171134 */
172135export function _asyncDebounce < T extends AnyAsyncFunction > (
173136 func : T ,
174137 wait : number ,
175- opt : DebounceOptions = { } ,
176- ) : T & Cancelable {
138+ opt : AsyncDebounceOptions = { } ,
139+ ) : AsyncDebounced < T > {
177140 let lastArgs : Parameters < T > | undefined
178141 let lastThis : ThisParameterType < T > | undefined
179142 let timerId : number | undefined
180- // The Promise shared by all calls coalesced into the next invocation.
181- // Undefined when there's no pending batch.
182- let deferred : DeferredPromise < Awaited < ReturnType < T > > > | undefined
143+ // Promise shared by all calls coalesced into the next invocation (undefined when none pending).
144+ let deferred : DeferredPromise < Awaited < ReturnType < T > > | undefined > | undefined
183145
184146 const { leading = false , trailing = true } = opt
185147
@@ -201,7 +163,7 @@ export function _asyncDebounce<T extends AnyAsyncFunction>(
201163 lastArgs = lastThis = undefined
202164 state . lastInvokeTime = time
203165
204- // Detach the current batch's Promise and settle it with this invocation's result .
166+ // Detach and settle the current batch .
205167 const d = deferred !
206168 deferred = undefined
207169 try {
@@ -212,7 +174,7 @@ export function _asyncDebounce<T extends AnyAsyncFunction>(
212174 }
213175 }
214176
215- async function leadingEdge ( time : number ) : Promise < Awaited < ReturnType < T > > > {
177+ async function leadingEdge ( time : number ) : Promise < Awaited < ReturnType < T > > | undefined > {
216178 // Reset any `maxWait` timer.
217179 state . lastInvokeTime = time
218180 // Start the timer for the trailing edge.
@@ -246,8 +208,7 @@ export function _asyncDebounce<T extends AnyAsyncFunction>(
246208 return
247209 }
248210 lastArgs = lastThis = undefined
249- // No invocation will happen for this batch (e.g. trailing=false) - resolve to
250- // undefined rather than leaving awaiters hanging forever.
211+ // Dropped batch (only reachable with `trailing: false`): no invocation, so resolve `undefined`.
251212 if ( deferred ) {
252213 const d = deferred
253214 deferred = undefined
@@ -262,7 +223,7 @@ export function _asyncDebounce<T extends AnyAsyncFunction>(
262223 state . lastInvokeTime = 0
263224 state . lastCallTime = undefined
264225 lastArgs = lastThis = timerId = undefined
265- // Reject pending awaiters so they don't hang .
226+ // Reject pending awaiters.
266227 if ( deferred ) {
267228 const d = deferred
268229 deferred = undefined
@@ -287,16 +248,15 @@ export function _asyncDebounce<T extends AnyAsyncFunction>(
287248 async function debounced (
288249 this : ThisParameterType < T > ,
289250 ...args : Parameters < T >
290- ) : Promise < Awaited < ReturnType < T > > > {
251+ ) : Promise < Awaited < ReturnType < T > > | undefined > {
291252 const time = Date . now ( )
292253 const isInvoking = shouldInvoke ( time , state )
293254
294255 lastArgs = args
295256 lastThis = this
296257 state . lastCallTime = time
297258
298- // Ensure a pending batch Promise exists for this call to join.
299- deferred ||= pDefer < Awaited < ReturnType < T > > > ( )
259+ deferred ||= pDefer < Awaited < ReturnType < T > > | undefined > ( )
300260 const currentDeferred = deferred
301261
302262 if ( isInvoking ) {
@@ -330,8 +290,8 @@ export function _asyncDebounce<T extends AnyAsyncFunction>(
330290export function _asyncThrottle < T extends AnyAsyncFunction > (
331291 func : T ,
332292 wait : number ,
333- opt : ThrottleOptions = { } ,
334- ) : T & Cancelable {
293+ opt : AsyncThrottleOptions = { } ,
294+ ) : AsyncDebounced < T > {
335295 return _asyncDebounce ( func , wait , {
336296 leading : true ,
337297 trailing : true ,
@@ -380,3 +340,92 @@ interface DebounceTimerState {
380340 maxing : boolean
381341 maxWait : number | undefined
382342}
343+
344+ export interface Cancelable {
345+ cancel : ( ) => void
346+ flush : ( ) => void
347+ }
348+
349+ export interface ThrottleOptions {
350+ /**
351+ * Invoke on the leading edge of the window (immediately, on the first call).
352+ *
353+ * @default true
354+ */
355+ leading ?: boolean
356+
357+ /**
358+ * Invoke on the trailing edge of the window (after `wait` has elapsed).
359+ *
360+ * @default true
361+ */
362+ trailing ?: boolean
363+ }
364+
365+ export interface DebounceOptions {
366+ /**
367+ * Invoke on the leading edge of the window (immediately, on the first call).
368+ *
369+ * @default false
370+ */
371+ leading ?: boolean
372+
373+ /**
374+ * Invoke on the trailing edge of the window (after `wait` has elapsed).
375+ *
376+ * @default true
377+ */
378+ trailing ?: boolean
379+
380+ /**
381+ * Maximum time `func` is allowed to be delayed before it's forcibly invoked.
382+ */
383+ maxWait ?: number
384+ }
385+
386+ export interface AsyncThrottleOptions {
387+ /**
388+ * Invoke on the leading edge of the window (immediately, on the first call).
389+ *
390+ * @default true
391+ */
392+ leading ?: boolean
393+
394+ /**
395+ * Invoke on the trailing edge of the window (after `wait` has elapsed).
396+ *
397+ * When `false`, calls dropped within the window (not served by a leading invocation) resolve
398+ * with `undefined`. There's no invocation for them to await. This is why the returned function
399+ * can resolve with `undefined` even though the original function's return type doesn't express that.
400+ *
401+ * @default true
402+ */
403+ trailing ?: boolean
404+ }
405+
406+ export interface AsyncDebounceOptions extends AsyncThrottleOptions {
407+ /**
408+ * Invoke on the leading edge of the window (immediately, on the first call).
409+ *
410+ * @default false
411+ */
412+ leading ?: boolean
413+
414+ /**
415+ * Maximum time `func` is allowed to be delayed before it's forcibly invoked.
416+ */
417+ maxWait ?: number
418+ }
419+
420+ /**
421+ * The function returned by `_asyncDebounce`/`_asyncThrottle`. Same signature as `T`, but resolves
422+ * `Awaited<ReturnType<T>> | undefined`
423+ *
424+ * This is because some configurations (e.g. `trailing: false`) may drop calls without invoking
425+ * the original function, so there's no result to await.
426+ */
427+ export type AsyncDebounced < T extends AnyAsyncFunction > = ( (
428+ this : ThisParameterType < T > ,
429+ ...args : Parameters < T >
430+ ) => Promise < Awaited < ReturnType < T > > | undefined > ) &
431+ Cancelable
0 commit comments