-
-
Notifications
You must be signed in to change notification settings - Fork 3k
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 respects throttleTime (…
- Loading branch information
1 parent
9d260e7
commit 338da80
Showing
3 changed files
with
186 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,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() | ||
} | ||
} | ||
} |
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() | ||
}) | ||
}) |