forked from TanStack/query
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(createAsyncStoragePersister): persistClient not respect throttleT…
…ime and not persisted final state queryClient (TanStack#3331)
- Loading branch information
alvin huang
committed
Mar 1, 2022
1 parent
02fd793
commit b6ab0fd
Showing
3 changed files
with
184 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
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() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
src/createAsyncStoragePersister/tests/asyncThrottle.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}) | ||
}) |