Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(curry): add leading option for function debounce and throttle #387

Closed
wants to merge 3 commits into from
Closed
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
11 changes: 8 additions & 3 deletions cdn/radash.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ const memoize = (cache, func, keyFunc, ttl) => {
const memo = (func, options = {}) => {
return memoize({}, func, options.key ?? null, options.ttl ?? null);
};
const debounce = ({ delay }, func) => {
const debounce = ({ delay, leading = false }, func) => {
let timer = void 0;
let active = true;
const debounced = (...args) => {
Expand All @@ -543,6 +543,10 @@ const debounce = ({ delay }, func) => {
active && func(...args);
timer = void 0;
}, delay);
if (leading) {
func(...args);
leading = false;
}
} else {
func(...args);
}
Expand All @@ -556,15 +560,16 @@ const debounce = ({ delay }, func) => {
debounced.flush = (...args) => func(...args);
return debounced;
};
const throttle = ({ interval }, func) => {
const throttle = ({ interval, leading = true }, func) => {
let ready = true;
let timer = void 0;
const throttled = (...args) => {
if (!ready)
return;
func(...args);
leading && func(...args);
ready = false;
timer = setTimeout(() => {
!leading && func(...args);
ready = true;
timer = void 0;
}, interval);
Expand Down
11 changes: 8 additions & 3 deletions cdn/radash.js
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ var radash = (function (exports) {
const memo = (func, options = {}) => {
return memoize({}, func, options.key ?? null, options.ttl ?? null);
};
const debounce = ({ delay }, func) => {
const debounce = ({ delay, leading = false }, func) => {
let timer = void 0;
let active = true;
const debounced = (...args) => {
Expand All @@ -546,6 +546,10 @@ var radash = (function (exports) {
active && func(...args);
timer = void 0;
}, delay);
if (leading) {
func(...args);
leading = false;
}
} else {
func(...args);
}
Expand All @@ -559,15 +563,16 @@ var radash = (function (exports) {
debounced.flush = (...args) => func(...args);
return debounced;
};
const throttle = ({ interval }, func) => {
const throttle = ({ interval, leading = true }, func) => {
let ready = true;
let timer = void 0;
const throttled = (...args) => {
if (!ready)
return;
func(...args);
leading && func(...args);
ready = false;
timer = setTimeout(() => {
!leading && func(...args);
ready = true;
timer = void 0;
}, interval);
Expand Down
2 changes: 1 addition & 1 deletion cdn/radash.min.js

Large diffs are not rendered by default.

31 changes: 19 additions & 12 deletions docs/curry/debounce.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,39 @@ description: Create a debounced callback function

## Basic usage

Debounce accepts an options object with a `delay` and a source function to call
Debounce accepts an options object with `delay`, `leading`, and a source function to call
when invoked. When the returned function is invoked it will only call the source
function after the `delay` milliseconds of time has passed. Calls that don't result
in invoking the source reset the delay, pushing off the next invocation.
in invoking the source reset the delay, pushing off the next invocation. The `leading`
option decides whether the source function is called on the first invocation of the debounce
function or not.

```ts
import { debounce } from 'radash'

const makeSearchRequest = (event) => {
const makeSearchRequest = event => {
api.movies.search(event.target.value)
}

input.addEventListener('change', debounce({ delay: 100 }, makeSearchRequest))
input.addEventListener(
'change',
debounce({ delay: 100, leading = true }, makeSearchRequest)
)
```

## Timing
## Timing & Leading

A visual of the debounce behavior when `delay` is `100`. The debounce function
returned by `debounce` can be called every millisecond but it will only call
the given callback after `delay` milliseconds have passed.
A visual of the debounce behavior when `delay` is `100` and `leading` is in different values.
The debounce function returned by `debounce` can be called every millisecond but it will only
call the given callback after `delay` milliseconds have passed. The `leading` option, `false`
by default, will call the source function immediately the first time the debounce function is
invoked when set to `true`.

```sh
Time: 0ms - - - - 100ms - - - - 200ms - - - - 300ms - - - - 400ms - - - -
debounce Invocations: x x x x - - - - - - - - x x x x x x x x x x - - - - - - - - - - - -
Source Invocations: - - - - - - - - - - x - - - - - - - - - - - - - - - - - x - - - - -
Time: 0ms - - - - 100ms - - - - 200ms - - - - 300ms - - - - 400ms - - - -
debounce Invocations: x x x x - - - - - - - - x x x x x x x x x x - - - - - - - - - - - -
Source Invocations(leading): x - - - - - - - - - x - - - - - - - - - - - - - - - - - x - - - - -
Source Invocations(not leading): - - - - - - - - - - x - - - - - - - - - - - - - - - - - x - - - - -
```

### Cancel
Expand Down Expand Up @@ -68,4 +76,3 @@ const debounced = debounce({ delay: 100 }, api.feed.refresh)

debounced.isPending()
```

33 changes: 19 additions & 14 deletions docs/curry/throttle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ group: 'Curry'
description: Create a throttled callback function
---




## Basic usage

Throttle accepts an options object with an `interval` and a source function to call
when invoked. When the returned function is invoked it will only call the source
function if the `interval` milliseconds of time has passed. Otherwise, it will ignore
the invocation.

the invocation. The `leading` option decides whether the source function is called on
the first invocation of the throttle function or not.

```ts
import { throttle } from 'radash'
Expand All @@ -22,24 +19,32 @@ const onMouseMove = () => {
rerender()
}

addEventListener('mousemove', throttle({ interval: 200 }, onMouseMove))
addEventListener(
'mousemove',
throttle({ interval: 200, leading = false }, onMouseMove)
)
```

## Timing
## Timing & Leading

A visual of the throttle behavior when `interval` is `200`. The throttle function
returned by `throttle` can be called every millisecond but it will only call
the given callback after `interval` milliseconds have passed.
A visual of the throttle behavior when `interval` is `200` and `leading` is in different
values. The throttle function returned by `throttle` can be called every millisecond but
it will only call the given callback after `interval` milliseconds have passed. The `leading`
option, `true` by default, will delay the execution cycle of source function by one interval
as a whole when set to `false`.

```sh
Time: 0ms - - - - 100ms - - - - 200ms - - - - 300ms - - - - 400ms - - - -
Throttle Invocations: x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x - - -
Source Invocations: x - - - - - - - - - - - - x - - - - - - - - - - - - - x - - - - - -
Time: 0ms - - - - 200ms - - - - 400ms - - - - 600ms - - - - 800ms - - - -
Throttle Invocations: x x x x x x x x x x x x x x x x x x x x x x x - - - - - - - - - - -
Source Invocations(leading): x - - - - - x - - - - - - x - - - - - - x - - - - - - - - - - - - -
Source Invocations(not leading): - - - - - - x - - - - - - x - - - - - - x - - - - - - x - - - - - -

```

### isThrottled

The function returned by `throttle` has a `isThrottled` method that when called will return if there is any active throttle.
The function returned by `throttle` has a `isThrottled` method that when called will return
if there is any active throttle.

```ts
const debounced = throttle({ interval: 200 }, onMouseMove)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "radash",
"version": "12.1.0",
"version": "12.1.1",
"description": "Functional utility library - modern, simple, typed, powerful",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.mjs",
Expand Down
38 changes: 35 additions & 3 deletions src/curry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,19 @@ export type DebounceFunction<TArgs extends any[]> = {
flush(...args: TArgs): void
}

export type DebounceConfig = {
/**
* The time in milliseconds to wait before calling the
* source function
*/
delay: number
/**
* whether the source function will be called on the first
* invocation of the debounce function. `false` by default
*/
leading?: boolean
}

export type ThrottledFunction<TArgs extends any[]> = {
(...args: TArgs): void
/**
Expand All @@ -502,6 +515,20 @@ export type ThrottledFunction<TArgs extends any[]> = {
isThrottled(): boolean
}

export type ThrottleConfig = {
/**
* The time in milliseconds to wait before calling the
* source function again.
*/
interval: number

/**
* whether the source function will be called on the first
* invocation of the debounce function. `true` by default
*/
leading?: boolean
}

/**
* Given a delay and a function returns a new function
* that will only call the source function after delay
Expand All @@ -512,7 +539,7 @@ export type ThrottledFunction<TArgs extends any[]> = {
* method to invoke them immediately
*/
export const debounce = <TArgs extends any[]>(
{ delay }: { delay: number },
{ delay, leading = false }: DebounceConfig,
func: (...args: TArgs) => any
) => {
let timer: NodeJS.Timeout | undefined = undefined
Expand All @@ -525,6 +552,10 @@ export const debounce = <TArgs extends any[]>(
active && func(...args)
timer = undefined
}, delay)
if (leading) {
func(...args)
leading = false
}
} else {
func(...args)
}
Expand All @@ -546,17 +577,18 @@ export const debounce = <TArgs extends any[]>(
* have passed since the last invocation
*/
export const throttle = <TArgs extends any[]>(
{ interval }: { interval: number },
{ interval, leading = true }: ThrottleConfig,
func: (...args: TArgs) => any
) => {
let ready = true
let timer: NodeJS.Timeout | undefined = undefined

const throttled: ThrottledFunction<TArgs> = (...args: TArgs) => {
if (!ready) return
func(...args)
leading && func(...args)
ready = false
timer = setTimeout(() => {
!leading && func(...args)
ready = true
timer = undefined
}, interval)
Expand Down
28 changes: 28 additions & 0 deletions src/tests/curry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,14 @@ describe('curry module', () => {
expect(mockFunc).toHaveBeenCalledTimes(1)
})

test('executes the function immediately on the first invocation of the debounce function when set `leading` to true', async () => {
func = _.debounce({ delay: 600, leading: true }, mockFunc)
runFunc3Times()
expect(mockFunc).toHaveBeenCalledTimes(1)
await _.sleep(610)
expect(mockFunc).toHaveBeenCalledTimes(2)
})

test('does not debounce after cancel is called', () => {
runFunc3Times()
expect(mockFunc).toHaveBeenCalledTimes(0)
Expand Down Expand Up @@ -395,6 +403,26 @@ describe('curry module', () => {
assert.equal(calls, 2)
})

test('leading option is set to `false`', async () => {
let calls = 0
const func = _.throttle({ interval: 600, leading: false }, () => calls++)
func()
func()
func()
await _.sleep(550)
assert.equal(calls, 0)
func()
func()
func()
await _.sleep(60)
func()
func()
func()
assert.equal(calls, 1)
await _.sleep(610)
assert.equal(calls, 2)
})

test('returns if the throttle is active', async () => {
const results = []
const func = _.throttle({ interval: 600 }, () => {})
Expand Down
Loading