Skip to content

Commit

Permalink
fix(createAsyncStoragePersister): persistClient respects throttleTime (
Browse files Browse the repository at this point in the history
  • Loading branch information
huanguolin committed Mar 1, 2022
1 parent 9d260e7 commit 338da80
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 40 deletions.
58 changes: 58 additions & 0 deletions src/createAsyncStoragePersister/asyncThrottle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
export interface AsyncThrottleOptions {
interval?: number
onError?: (error: unknown) => void
}

const noop = () => {
/* do nothing */
}

export function asyncThrottle<Args extends readonly unknown[]>(
func: (...args: Args) => Promise<void>,
{ interval = 1000, onError = noop }: AsyncThrottleOptions = {}
) {
if (typeof func !== 'function') throw new Error('argument is not function.')

let running = false
let lastTime = 0
let timeout: ReturnType<typeof setTimeout>
let currentArgs: Args | null = null

const execFunc = async () => {
if (currentArgs) {
const args = currentArgs
currentArgs = null
try {
running = true
await func(...args)
} catch (error) {
onError(error)
} finally {
lastTime = Date.now() // this line must after 'func' executed to avoid two 'func' running in concurrent.
running = false
}
}
}

const delayFunc = async () => {
clearTimeout(timeout)
timeout = setTimeout(() => {
if (running) {
delayFunc() // Will come here when 'func' execution time is greater than the interval.
} else {
execFunc()
}
}, interval)
}

return (...args: Args) => {
currentArgs = args

const tooSoon = Date.now() - lastTime < interval
if (running || tooSoon) {
delayFunc()
} else {
execFunc()
}
}
}
41 changes: 1 addition & 40 deletions src/createAsyncStoragePersister/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PersistedClient, Persister } from '../persistQueryClient'
import { asyncThrottle } from './asyncThrottle'

interface AsyncStorage {
getItem: (key: string) => Promise<string | null>
Expand Down Expand Up @@ -50,43 +51,3 @@ export const createAsyncStoragePersister = ({
removeClient: () => storage.removeItem(key),
}
}

function asyncThrottle<Args extends readonly unknown[], Result>(
func: (...args: Args) => Promise<Result>,
{ interval = 1000, limit = 1 }: { interval?: number; limit?: number } = {}
) {
if (typeof func !== 'function') throw new Error('argument is not function.')
const running = { current: false }
let lastTime = 0
let timeout: ReturnType<typeof setTimeout>
const queue: Array<Args> = []
return (...args: Args) =>
(async () => {
if (running.current) {
lastTime = Date.now()
if (queue.length > limit) {
queue.shift()
}

queue.push(args)
clearTimeout(timeout)
}
if (Date.now() - lastTime > interval) {
running.current = true
await func(...args)
lastTime = Date.now()
running.current = false
} else {
if (queue.length > 0) {
const lastArgs = queue[queue.length - 1]!
timeout = setTimeout(async () => {
if (!running.current) {
running.current = true
await func(...lastArgs)
running.current = false
}
}, interval)
}
}
})()
}
127 changes: 127 additions & 0 deletions src/createAsyncStoragePersister/tests/asyncThrottle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { asyncThrottle } from '../asyncThrottle'
import { sleep as delay } from '../../reactjs/tests/utils'

describe('asyncThrottle', () => {
test('basic', async () => {
const interval = 10
const execTimeStamps: number[] = []
const mockFunc = jest.fn(
async (id: number, complete?: (value?: unknown) => void) => {
await delay(1)
execTimeStamps.push(Date.now())
if (complete) {
complete(id)
}
}
)
const testFunc = asyncThrottle(mockFunc, { interval })

testFunc(1)
await delay(1)
testFunc(2)
await delay(1)
await new Promise(resolve => testFunc(3, resolve))

expect(mockFunc).toBeCalledTimes(2)
expect(mockFunc.mock.calls[1]?.[0]).toBe(3)
expect(execTimeStamps.length).toBe(2)
expect(execTimeStamps[1]! - execTimeStamps[0]!).toBeGreaterThanOrEqual(
interval
)
})

test('Bug #3331 case 1: Special timing', async () => {
const interval = 1000
const execTimeStamps: number[] = []
const mockFunc = jest.fn(
async (id: number, complete?: (value?: unknown) => void) => {
await delay(30)
execTimeStamps.push(Date.now())
if (complete) {
complete(id)
}
}
)
const testFunc = asyncThrottle(mockFunc, { interval })

testFunc(1)
testFunc(2)
await delay(35)
testFunc(3)
await delay(35)
await new Promise(resolve => testFunc(4, resolve))

expect(mockFunc).toBeCalledTimes(2)
expect(mockFunc.mock.calls[1]?.[0]).toBe(4)
expect(execTimeStamps.length).toBe(2)
expect(execTimeStamps[1]! - execTimeStamps[0]!).toBeGreaterThanOrEqual(
interval
)
})

test('Bug #3331 case 2: "func" execution time is greater than the interval.', async () => {
const interval = 1000
const execTimeStamps: number[] = []
const mockFunc = jest.fn(
async (id: number, complete?: (value?: unknown) => void) => {
await delay(interval + 10)
execTimeStamps.push(Date.now())
if (complete) {
complete(id)
}
}
)
const testFunc = asyncThrottle(mockFunc, { interval })

testFunc(1)
testFunc(2)
await new Promise(resolve => testFunc(3, resolve))

expect(mockFunc).toBeCalledTimes(2)
expect(mockFunc.mock.calls[1]?.[0]).toBe(3)
expect(execTimeStamps.length).toBe(2)
expect(execTimeStamps[1]! - execTimeStamps[0]!).toBeGreaterThanOrEqual(
interval
)
})

test('"func" throw error not break next invoke', async () => {
const mockFunc = jest.fn(
async (id: number, complete?: (value?: unknown) => void) => {
if (id === 1) throw new Error('error')
await delay(1)
if (complete) {
complete(id)
}
}
)
const testFunc = asyncThrottle(mockFunc, { interval: 10 })

testFunc(1)
await delay(1)
await new Promise(resolve => testFunc(2, resolve))

expect(mockFunc).toBeCalledTimes(2)
expect(mockFunc.mock.calls[1]?.[0]).toBe(2)
})

test('"onError" should be called when "func" throw error', done => {
const err = new Error('error')
const handleError = (e: unknown) => {
expect(e).toBe(err)
done()
}

const testFunc = asyncThrottle(
() => {
throw err
},
{ onError: handleError }
)
testFunc()
})

test('should throw error when "func" is not a function', () => {
expect(() => asyncThrottle(1 as any)).toThrowError()
})
})

0 comments on commit 338da80

Please sign in to comment.