Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/pacer-lite/src/lite-debouncer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ export class LiteDebouncer<TFn extends AnyFunction> {
this.options.onExecute?.(args, this)
}

this.lastArgs = args
// Only store args that were not already handled by the leading edge,
// so a later flush or trailing execution cannot re-execute them
if (!didLeadingExecute) {
this.lastArgs = args
}

if (this.timeoutId) {
clearTimeout(this.timeoutId)
Expand Down
17 changes: 17 additions & 0 deletions packages/pacer-lite/tests/lite-debouncer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,23 @@ describe('LiteDebouncer', () => {
expect(mockFn).toHaveBeenLastCalledWith('second')
})

it('should not re-execute args already handled by the leading edge', () => {
const mockFn = vi.fn()
const debouncer = new LiteDebouncer(mockFn, {
wait: 1000,
leading: true,
trailing: true,
})

debouncer.maybeExecute('only-call')
expect(mockFn).toBeCalledTimes(1)

// The single call was fully handled by the leading edge, so flush
// must not execute it a second time
debouncer.flush()
expect(mockFn).toBeCalledTimes(1)
})

it('should flush pending execution even with trailing: false', () => {
const mockFn = vi.fn()
const debouncer = new LiteDebouncer(mockFn, {
Expand Down
6 changes: 4 additions & 2 deletions packages/pacer/src/debouncer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,10 @@ export class Debouncer<TFn extends AnyFunction> {
this.#execute(...args)
}

// Start pending state to indicate that the debouncer is waiting for the trailing edge
if (this.options.trailing) {
// Start pending state to indicate that the debouncer is waiting for the trailing edge.
// Skip if this call already executed on the leading edge, since the timeout
// will not perform a trailing execution for it.
if (this.options.trailing && !_didLeadingExecute) {
this.#setState({ isPending: true, lastArgs: args })
}

Expand Down
39 changes: 39 additions & 0 deletions packages/pacer/tests/debouncer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,24 @@ describe('Debouncer', () => {
expect(mockFn).toHaveBeenLastCalledWith('second')
})

it('should not re-execute stale args after a single leading execution', () => {
const mockFn = vi.fn()
const debouncer = new Debouncer(mockFn, {
wait: 1000,
leading: true,
trailing: true,
})

debouncer.maybeExecute('only-call')
expect(mockFn).toBeCalledTimes(1)
vi.advanceTimersByTime(1000)

// The single call was fully handled by the leading edge, so there is
// nothing pending for flush to execute
debouncer.flush()
expect(mockFn).toBeCalledTimes(1)
})

it('should not work with leading-only execution because there would be no trailing execution to flush', () => {
const mockFn = vi.fn()
const debouncer = new Debouncer(mockFn, {
Expand Down Expand Up @@ -480,6 +498,27 @@ describe('Debouncer', () => {
expect(debouncer.store.state.isPending).toBe(false)
})

it('should not be pending after a single leading-only execution', () => {
const mockFn = vi.fn()
const debouncer = new Debouncer(mockFn, {
wait: 1000,
leading: true,
trailing: true,
})

// A single call executes on the leading edge; no trailing execution
// is owed, so the debouncer should not report itself as pending
debouncer.maybeExecute('only-call')
expect(mockFn).toBeCalledTimes(1)
expect(debouncer.store.state.isPending).toBe(false)
expect(debouncer.store.state.lastArgs).toBeUndefined()

vi.advanceTimersByTime(1000)
expect(mockFn).toBeCalledTimes(1)
expect(debouncer.store.state.isPending).toBe(false)
expect(debouncer.store.state.status).toBe('idle')
})

it('should not be pending when leading and trailing are both false', () => {
const mockFn = vi.fn()
const debouncer = new Debouncer(mockFn, {
Expand Down