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
37 changes: 23 additions & 14 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 @@ -10,9 +10,11 @@ lines, less than 350 bytes minified+gzipped)
```js
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
middle?: boolean, // fire during a sequence of calls when `wait` has passed
end?: boolean, // fire after the end of a sequence of calls
once?: boolean, // cancel after the first successful call, never calling again
}

function throttle<T>(
callback: (...args: T[]) => any,
wait: number,
Expand All @@ -31,8 +33,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 @@ -44,19 +46,26 @@ 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}) | 3 5 7 9 10 |
| throttle(fn, 100, {middle: false}) | 1 10 |
| throttle(fn, 100, {end: false}) | 1 3 5 7 9 |
| throttle(fn, 100, {start: false, middle: false}) | 10 |
| throttle(fn, 100, {middle: false, end: false}) | 1 |
| throttle(fn, 100, {start: false, end: false}) | 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!
**Note**: `wait` is the _minimum_ amount of time between calls. For some configurations, the actual wait time may be longer. For example, when `middle` is `false` and `end` is `true`, the end call must wait longer than otherwise to ensure that no calls are made afterwards which would turn it into a middle call.

### 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
2 changes: 1 addition & 1 deletion decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {debounce as db, throttle as th, ThrottleOptions} from './index'
export function throttle(wait = 0, opts: ThrottleOptions = {}): MethodDecorator {
return (proto: unknown, name: string | symbol, descriptor: PropertyDescriptor) => {
if (!descriptor || typeof descriptor.value !== 'function') {
throw new Error('debounce can only decorate functions')
throw new Error('throttle can only decorate functions')
}
const fn = descriptor.value
descriptor.value = th(fn, wait, opts)
Expand Down
19 changes: 0 additions & 19 deletions index.js.flow

This file was deleted.

95 changes: 67 additions & 28 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,107 @@
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>
/** Time of last made call in this sequence, or time of start of sequence if a call hasn't been made yet. */
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
}

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 queueCall = (delay: number) => {
clearTimeout(endTimer)
endTimer = setTimeout(() => makeCall(), delay)
}

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

if (isStartOfSequence) lastCall = Date.now()

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

// If middle and end are both enabled, we can queue them the same way. This ensures even spacing; calls are made
// exactly every `wait` ms during the sequence. Mimics Lodash style throttle.
if (middle && end) {
const remainingWait = wait - (Date.now() - lastCall)
if (remainingWait <= 0) {
makeCall()
} else {
// +1 ms ensures that if a call is attempted at exactly the same time as the queued call would be made,
// the requested call will win over the queued call.
queueCall(remainingWait + 1)
}
return
}

// If middle is disabled (something Lodash doesn't support), we have to wait the full interval for the end call.
// This is because we don't know if another call will be made unless we wait until the sequence is definitely over.
if (!middle && end) {
queueCall(wait)
return
}

// If middle is enabled but end is disabled, we cannot make the middle call until another call is made - if we
// optimistically make a call as soon as `wait` is over, it will look like an `end` call which would violate the end setting.
if (middle && !end) {
if (Date.now() - lastCall >= wait) makeCall()
return
}
}

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