Skip to content

Commit

Permalink
feat: throttle function
Browse files Browse the repository at this point in the history
  • Loading branch information
KiraLT committed Jun 21, 2023
1 parent 30451d4 commit 9983e9b
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 9 deletions.
55 changes: 54 additions & 1 deletion spec/async.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { delay, debounce } from '../src'
import { delay, debounce, throttle } from '../src'

describe('delay', () => {
it('delay action', async () => {
Expand Down Expand Up @@ -30,3 +30,56 @@ describe('debounce', () => {
expect(mock.mock.calls.length).toBe(1)
})
})

describe('throttle', () => {
it('should throttle function invocation', async () => {
const mock = jest.fn()
const cb = throttle(mock, 100)

cb(1)
expect(mock.mock.calls.length).toBe(1)

await delay(90)

cb(2)
expect(mock.mock.calls.length).toBe(1)

await delay(10)

expect(mock.mock.calls.length).toBe(2)
})

it('without leading', async () => {
const mock = jest.fn()
const cb = throttle(mock, 100, { leading: false })

cb(1)
expect(mock.mock.calls.length).toBe(0)

await delay(90)

cb(2)
expect(mock.mock.calls.length).toBe(0)

await delay(10)

expect(mock.mock.calls.length).toBe(1)
})

it('without trailing', async () => {
const mock = jest.fn()
const cb = throttle(mock, 100, { trailing: false })

cb(1)
expect(mock.mock.calls.length).toBe(1)

await delay(90)

cb(2)
expect(mock.mock.calls.length).toBe(1)

await delay(10)

expect(mock.mock.calls.length).toBe(1)
})
})
10 changes: 8 additions & 2 deletions spec/files.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { formatBytes, getFileParts, parseSize, getMimeType, getExtension } from '../src'
import {
formatBytes,
getFileParts,
parseSize,
getMimeType,
getExtension,
} from '../src'

describe('formatBytes', () => {
it('handles zero', () => {
Expand Down Expand Up @@ -115,4 +121,4 @@ describe('getExtension', () => {
it('returns extension for json', () => {
expect(getExtension('application/json')).toEqual('json')
})
})
})
94 changes: 90 additions & 4 deletions src/async.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
/**
* Returns `Promise` delayed for specified time in MS.
* Returns a promise that resolves after the specified number of milliseconds.
*
* @group Async
* @example
* ```
* await delay(1000) // wait for 1 second
* ```
*/
export function delay(timeInMs: number): Promise<void> {
export function delay(
/**
* The number of milliseconds to delay.
*/
timeInMs: number
): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
Expand All @@ -16,16 +25,93 @@ export function delay(timeInMs: number): Promise<void> {
* until after wait milliseconds have elapsed since the last time it was invoked.
*
* @group Async
* ```
* const debounced = debounce(() => console.log('debounced'), 100)
* debounced() // will be called after 100ms
* ```
*/
export function debounce<A extends unknown[]>(
func: (...args: A) => unknown,
/**
* The function to be debounced.
*/
func: (...args: A) => void,
/**
* The number of milliseconds to delay.
*/
timeInMs: number
): (...args: A) => void {
let timer: NodeJS.Timeout
let timer: ReturnType<typeof setTimeout>
return function (this: unknown, ...args: A) {
clearTimeout(timer)
timer = setTimeout(async () => {
func.apply(this, args)
}, timeInMs)
}
}

/**
* Creates a throttled function that will execute `func` at most once per `timeInMs` interval.
*
* @group Async
* @example
* ```
* const throttled = throttle(() => console.log('throttled'), 100)
* throttled() // will be called immediately
* throttled() // will be ignored
* await delay(100)
* throttled() // will be called after 100ms
* ```
*/
export function throttle<A extends unknown[]>(
/**
* The function to be throttled.
*/
func: (...args: A) => void,
/**
* The number of milliseconds to throttle invocations to.
*/
timeInMs: number,
/**
* Options object.
*/
options?: {
/**
* If `true` the `func` will be executed on the leading edge of the timeout.
* @defaultValue `true`
*/
leading?: boolean
/**
* If `true` the `func` will be executed on the trailing edge of the timeout.
* @defaultValue `true`
*/
trailing?: boolean
}
): (...args: A) => void {
let lastCallTime: number | undefined
let timer: ReturnType<typeof setTimeout> | undefined
let lastArgs: A
let lastThis: unknown

const { leading = true, trailing = true } = options ?? {}

return function (this: unknown, ...args: A) {
const now = Date.now()
lastArgs = args
lastThis = this

if (leading && lastCallTime === undefined) {
lastCallTime = now
func.apply(lastThis, lastArgs)
} else if (lastCallTime !== undefined && now - (lastCallTime ?? 0) >= timeInMs) {
clearTimeout(timer)
lastCallTime = now
func.apply(lastThis, lastArgs)
} else if (trailing && !timer) {
timer = setTimeout(() => {
lastCallTime = now
func.apply(lastThis, lastArgs)
timer = undefined
}, lastCallTime === undefined ? timeInMs : timeInMs - (now - lastCallTime))
}
}
}
10 changes: 10 additions & 0 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { hashCode } from '.'

/**
* Caches the result of a function call by its arguments.
*
* @example
* ```
* const cached = cache(() => Math.random())
* cached() // will be called immediately
* cached() // will return the same value
* ```
*/
export function cache<T extends Function>(func: T): T {
const cachedCalls: Record<number, unknown> = {}

Expand Down
4 changes: 2 additions & 2 deletions src/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ export function getMimeType(name: string): string | undefined {

/**
* Returns file extension from mime type
*
*
* @example
* ```
* getExtension('text/plain') // txt
Expand All @@ -391,4 +391,4 @@ export function getMimeType(name: string): string | undefined {
*/
export function getExtension(mimeType: string): string | undefined {
return mimeTypes[mimeType]?.extensions[0]
}
}

0 comments on commit 9983e9b

Please sign in to comment.