Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
26 changes: 14 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# mini-throttle

This is a package which provides `throttle` and `debounce` functions, with both
flow and TypeScript declarations, and a minimal code footprint (less than 60
flow and TypeScript declarations, and a minimal code footprint (less than 90
lines, less than 350 bytes minified+gzipped)


Expand All @@ -11,7 +11,8 @@ lines, less than 350 bytes minified+gzipped)
type ThrottleOptions = {
start?: boolean, // fire immediately on the first call
middle?: boolean, // if true, fire as soon as `wait` has passed
once?: boolean, // cancel after the first successful call
end?: boolean, // if true, fire after `wait` has passed since last call
once?: boolean, // cancel after the first successful call, never calling again
}
function throttle<T>(
callback: (...args: T[]) => any,
Expand All @@ -31,8 +32,8 @@ This package comes with two functions; `throttle` and `debounce`.
Both of these functions offer the exact same signature, because they're both
the same function - just with different `opts` defaults:

- `throttle` opts default to `{ start: true, middle: true, once: false }`.
- `debounce` opts default to `{ start: false, middle: false, once: false }`.
- `throttle` opts default to `{ start: true, middle: true, end: true, once: false }`.
- `debounce` opts default to `{ start: false, middle: false, end: true, once: false }`.

Each of the options changes when `callback` gets called. The best way to
illustrate this is with a marble diagram.
Expand All @@ -45,18 +46,19 @@ for (let i = 1; i <= 10; ++i) {
await delay(100)
```
```
| fn() | 1 2 3 4 5 6 7 8 9 10 |
| throttle(fn, 100) | 1 2 4 6 8 10 |
| throttle(fn, 100, {start: false}) | 2 4 6 8 10 |
| throttle(fn, 100, {middle: false}) | 1 10 |
| throttle(fn, 100, {once: true}) | 1 |
| throttle(fn, 100, {once: true, start: false})| 2 |
| debounce(fn, 100) | 10 |
| fn() | 1 2 3 4 5 6 7 8 9 10 |
| throttle(fn, 100) | 1 3 5 7 9 10 |
| throttle(fn, 100, {start: false}) | 2 4 6 8 10 |
| throttle(fn, 100, {middle: false}) | 1 10 |
| throttle(fn, 100, {end: false}) | 1 3 5 7 9 |
| throttle(fn, 100, {once: true}) | 1 |
| throttle(fn, 100, {once: true, start: false})| 3 |
| debounce(fn, 100) | 10 |
```

### TypeScript Decorators Support!

This package also includes a decorator module which can be used to provide [TypeScript Decorator](https://www.typescriptlang.org/docs/handbook/decorators.html#decorators) annotations to functions.
This package also includes a decorator module which can be used to provide [TypeScript Decorator](https://www.typescriptlang.org/docs/handbook/decorators.html#decorators) annotations to class methods.

Here's an example, showing what you need to do:

Expand Down
73 changes: 44 additions & 29 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,83 @@
export interface ThrottleOptions {
/**
* Fire immediately on the first call.
* Fire at the start of a sequence of calls.
*/
start?: boolean
/**
* Fire as soon as `wait` has passed.
* Fire in the middle of a sequence of calls when `wait` has passed.
*/
middle?: boolean
/**
* Cancel after the first successful call.
* Fire at the end of a sequence of calls.
*/
end?: boolean
/**
* Cancel after the first successful call. Will never call again.
*/
once?: boolean
}

interface Throttler<T extends unknown[]> {
(...args: T): void
/** Cancel any pending calls and never call the function again. */
cancel(): void
}

export function throttle<T extends unknown[]>(
callback: (...args: T) => unknown,
wait = 0,
{start = true, middle = true, once = false}: ThrottleOptions = {}
{start = true, middle = true, once = false, end = true}: ThrottleOptions = {}
): Throttler<T> {
let innerStart = start
let last = 0
let timer: ReturnType<typeof setTimeout>
let lastCall = 0
let lastAttempt = 0
let endTimer: ReturnType<typeof setTimeout>
let cancelled = false
function fn(this: unknown, ...args: T) {
if (cancelled) return
const delta = Date.now() - last
last = Date.now()

if (start && middle && delta >= wait) {
innerStart = true
}
function fn(this: Throttler<T>, ...args: T) {
if (cancelled) return

if (innerStart) {
innerStart = false
const makeCall = () => {
clearTimeout(endTimer)
lastCall = Date.now()
callback.apply(this, args)
if (once) fn.cancel()
} else if ((middle && delta < wait) || !middle) {
clearTimeout(timer)
timer = setTimeout(
() => {
last = Date.now()
callback.apply(this, args)
if (once) fn.cancel()
},
!middle ? wait : wait - delta
)
}

const isStartOfSequence = Date.now() - lastAttempt >= wait
lastAttempt = Date.now()

// start: If it has been a sufficient time since the last attempt, start a new sequence
if (start && isStartOfSequence) {
makeCall()
return
}

// middle: If it has been sufficient time since the last successful call, make another call
if (middle && Date.now() - lastCall >= wait && !isStartOfSequence) {
Comment thread
iansan5653 marked this conversation as resolved.
Outdated
makeCall()
return
}

// end: Schedule a call for after the sequence ends, if it hasn't already been handled
if (end) {
clearTimeout(endTimer)
endTimer = setTimeout(() => makeCall(), wait)
}
}

fn.cancel = () => {
clearTimeout(timer)
clearTimeout(endTimer)
cancelled = true
}

return fn
}

// debounce is just throttle but with only an ending call
export function debounce<T extends unknown[]>(
callback: (...args: T) => unknown,
wait = 0,
{start = false, middle = false, once = false}: ThrottleOptions = {}
{start = false, middle = false, once = false, end = true}: ThrottleOptions = {}
): Throttler<T> {
return throttle(callback, wait, {start, middle, once})
return throttle(callback, wait, {start, middle, once, end})
}
Loading