-
-
Notifications
You must be signed in to change notification settings - Fork 44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Expose timers API #327
base: main
Are you sure you want to change the base?
Expose timers API #327
Changes from all commits
520a790
cf55ce7
778c8b2
c1ec0e5
51ed264
2dbde5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,11 +7,11 @@ import { | |
createEvent, | ||
createStore, | ||
sample, | ||
createWatch, | ||
} from 'effector'; | ||
createWatch, createEffect, | ||
} from 'effector' | ||
import { wait, watch } from '../../test-library'; | ||
|
||
import { debounce } from './index'; | ||
import { debounce, DebounceTimerFxProps } from './index' | ||
|
||
test('debounce works in forked scope', async () => { | ||
const app = createDomain(); | ||
|
@@ -186,19 +186,23 @@ describe('edge cases', () => { | |
test('does not call target twice for sample chain doubles', async () => { | ||
const trigger = createEvent(); | ||
|
||
const scope = fork(); | ||
const db = debounce({ source: trigger, timeout: 100 }); | ||
|
||
const listener = jest.fn(); | ||
db.watch(listener); | ||
|
||
createWatch({ | ||
unit: db, | ||
fn: listener, | ||
scope, | ||
}) | ||
Comment on lines
+194
to
+198
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be moved just before |
||
|
||
const start = createEvent(); | ||
const secondTrigger = createEvent(); | ||
|
||
sample({ clock: start, fn: () => 'one', target: [secondTrigger, trigger] }); | ||
sample({ clock: secondTrigger, fn: () => 'two', target: [trigger] }); | ||
|
||
const scope = fork(); | ||
|
||
await allSettled(start, { scope }); | ||
|
||
expect(listener).toBeCalledTimes(1); | ||
|
@@ -232,3 +236,44 @@ describe('edge cases', () => { | |
expect(triggerListener).toBeCalledTimes(0); | ||
}) | ||
}); | ||
|
||
test('exposed timers api', async () => { | ||
const timerFx = createEffect(({ canceller, timeout }: DebounceTimerFxProps) => { | ||
const { timeoutId, rejectPromise } = canceller; | ||
|
||
if (timeoutId) clearTimeout(timeoutId); | ||
if (rejectPromise) rejectPromise(); | ||
|
||
return new Promise((resolve, reject) => { | ||
canceller.timeoutId = setTimeout(resolve, timeout / 2); | ||
canceller.rejectPromise = reject; | ||
}); | ||
}); | ||
|
||
const scope = fork({ | ||
handlers: [ | ||
[debounce.timerFx, timerFx], | ||
] | ||
}); | ||
|
||
const mockedFn = jest.fn(); | ||
|
||
const clock = createEvent(); | ||
const tick = debounce(clock, 50); | ||
|
||
createWatch({ | ||
unit: tick, | ||
fn: mockedFn, | ||
scope, | ||
}); | ||
|
||
allSettled(clock, { scope }); | ||
|
||
await wait(20); | ||
|
||
expect(mockedFn).not.toBeCalled(); | ||
|
||
await wait(5); | ||
|
||
expect(mockedFn).toBeCalled(); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,18 +9,38 @@ import { | |
merge, | ||
UnitTargetable, | ||
EventAsReturnType, | ||
createEffect | ||
} from 'effector'; | ||
|
||
export function debounce<T>( | ||
type DebounceCanceller = { timeoutId?: NodeJS.Timeout; rejectPromise?: () => void; }; | ||
|
||
export type DebounceTimerFxProps = { | ||
canceller: DebounceCanceller; | ||
timeout: number; | ||
}; | ||
|
||
const timerFx = createEffect(({ canceller, timeout }: DebounceTimerFxProps) => { | ||
const { timeoutId, rejectPromise } = canceller; | ||
|
||
if (timeoutId) clearTimeout(timeoutId); | ||
if (rejectPromise) rejectPromise(); | ||
|
||
return new Promise((resolve, reject) => { | ||
canceller.timeoutId = setTimeout(resolve, timeout); | ||
canceller.rejectPromise = reject; | ||
}); | ||
}); | ||
|
||
export function _debounce<T>( | ||
source: Unit<T>, | ||
timeout: number | Store<number>, | ||
): EventAsReturnType<T>; | ||
export function debounce<T>(_: { | ||
export function _debounce<T>(_: { | ||
source: Unit<T>; | ||
timeout: number | Store<number>; | ||
name?: string; | ||
}): EventAsReturnType<T>; | ||
export function debounce< | ||
export function _debounce< | ||
T, | ||
Target extends UnitTargetable<T> | UnitTargetable<void>, | ||
>(_: { | ||
|
@@ -29,7 +49,7 @@ export function debounce< | |
target: Target; | ||
name?: string; | ||
}): Target; | ||
export function debounce<T>( | ||
export function _debounce<T>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this export of |
||
...args: | ||
| [ | ||
{ | ||
|
@@ -50,25 +70,20 @@ export function debounce<T>( | |
|
||
const $timeout = toStoreNumber(timeout); | ||
|
||
const saveCancel = createEvent<[NodeJS.Timeout, () => void]>(); | ||
const $canceller = createStore<[NodeJS.Timeout, () => void] | []>([], { | ||
const $canceller = createStore<DebounceCanceller | null>(null, { | ||
serialize: 'ignore', | ||
}).on(saveCancel, (_, payload) => payload); | ||
}); | ||
|
||
const tick = (target as UnitTargetable<T>) ?? createEvent(); | ||
|
||
const timerFx = attach({ | ||
const innerTimerFx = attach({ | ||
name: name || `debounce(${(source as any)?.shortName || source.kind}) effect`, | ||
source: $canceller, | ||
effect([timeoutId, rejectPromise], timeout: number) { | ||
if (timeoutId) clearTimeout(timeoutId); | ||
if (rejectPromise) rejectPromise(); | ||
return new Promise((resolve, reject) => { | ||
saveCancel([setTimeout(resolve, timeout), reject]); | ||
}); | ||
}, | ||
source: $canceller as Store<DebounceCanceller>, | ||
mapParams: (timeout: number, source: DebounceCanceller) => ({ canceller: source, timeout }), | ||
effect: timerFx, | ||
}); | ||
$canceller.reset(timerFx.done); | ||
|
||
$canceller.reset(innerTimerFx.done); | ||
|
||
// It's ok - nothing will ever start unless source is triggered | ||
const $payload = createStore<T[]>([], { serialize: 'ignore', skipVoid: false }).on( | ||
|
@@ -88,15 +103,15 @@ export function debounce<T>( | |
// debounce timeout should be restarted on timeout change | ||
$timeout, | ||
// debounce timeout can be restarted in later ticks | ||
timerFx, | ||
innerTimerFx, | ||
], | ||
() => true, | ||
); | ||
|
||
const requestTick = merge([ | ||
source, | ||
// debounce timeout is restarted on timeout change | ||
sample({ clock: $timeout, filter: timerFx.pending }), | ||
sample({ clock: $timeout, filter: innerTimerFx.pending }), | ||
]); | ||
|
||
sample({ | ||
|
@@ -106,21 +121,33 @@ export function debounce<T>( | |
}); | ||
|
||
sample({ | ||
clock: triggerTick, | ||
source: $timeout, | ||
fn: (timeout) => timeout, | ||
target: innerTimerFx, | ||
}); | ||
|
||
sample({ | ||
clock: triggerTick, | ||
target: timerFx, | ||
source: $canceller, | ||
fn: (canceller) => canceller ?? {}, | ||
target: $canceller, | ||
}); | ||
|
||
sample({ | ||
source: $payload, | ||
clock: timerFx.done, | ||
clock: innerTimerFx.done, | ||
fn: ([payload]) => payload, | ||
target: tick, | ||
}); | ||
|
||
return tick as any; | ||
} | ||
|
||
export const debounce = Object.assign(_debounce, { | ||
timerFx | ||
}); | ||
|
||
function toStoreNumber(value: number | Store<number> | unknown): Store<number> { | ||
if (is.store(value)) return value; | ||
if (typeof value === 'number') { | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -197,3 +197,32 @@ someHappened(4); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// someHappened now 4 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
### [Tests] Exposed timers API example | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
```ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* `canceller` - is object, which contains previous timeout id and previous effect promise reject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const timerFx = createEffect(({ canceller, timeout }: DebounceTimerFxProps) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { timeoutId, rejectPromise } = canceller; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (timeoutId) clearTimeout(timeoutId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (rejectPromise) rejectPromise(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
canceller.timeoutId = setTimeout(resolve, timeout); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
canceller.rejectPromise = reject; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const scope = fork({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
handlers: [[debounce.timerFx, timerFx]], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+207
to
+221
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
handler should be function |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const clock = createEvent(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const tick = debounce(clock, 200); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// important! call from scope | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
allSettled(clock, { scope }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scope should be created AFTER the model initialization.