-
-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(interval): implement basic interval
- Loading branch information
1 parent
97e47ef
commit 503bc43
Showing
3 changed files
with
230 additions
and
0 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,9 @@ | ||
import { Event, Store } from 'effector'; | ||
|
||
export function interval(config: { | ||
timeout: number | Store<number>; | ||
leading?: boolean; | ||
trailing?: boolean; | ||
start: Event<void>; | ||
stop?: Event<void>; | ||
}): { tick: Event<void>; opened: Store<boolean> }; |
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,75 @@ | ||
const { | ||
createEffect, | ||
createStore, | ||
createEvent, | ||
sample, | ||
forward, | ||
guard, | ||
is, | ||
} = require('effector'); | ||
|
||
function interval({ timeout, start, stop, leading = false, trailing = false }) { | ||
const tick = createEvent(); | ||
const $timeout = toStoreNumber(timeout); | ||
const $opened = createStore(false); | ||
|
||
let timeoutId; | ||
|
||
const timeoutFx = createEffect((timeout) => { | ||
return new Promise((resolve) => { | ||
timeoutId = setTimeout(resolve, timeout); | ||
}); | ||
}); | ||
|
||
const cleanupFx = createEffect(() => { | ||
clearTimeout(timeoutId); | ||
}); | ||
|
||
$opened.on(start, () => true); | ||
|
||
sample({ | ||
clock: start, | ||
source: $timeout, | ||
target: timeoutFx, | ||
}); | ||
|
||
if (leading) forward({ from: start, to: tick }); | ||
|
||
guard({ | ||
clock: timeoutFx.done, | ||
filter: $opened, | ||
source: $timeout, | ||
target: timeoutFx, | ||
}); | ||
|
||
sample({ | ||
clock: timeoutFx.done, | ||
fn: () => {}, | ||
target: tick, | ||
}); | ||
|
||
if (stop) { | ||
if (trailing) forward({ from: stop, to: tick }); | ||
$opened.on(stop, () => false); | ||
forward({ from: stop, to: cleanupFx }); | ||
} | ||
|
||
return { tick, opened: $opened }; | ||
} | ||
|
||
module.exports = { interval }; | ||
|
||
function toStoreNumber(value) { | ||
if (is.store(value)) return value; | ||
if (typeof value === 'number') { | ||
if (!Number.isFinite(value) || value < 0) { | ||
throw new TypeError( | ||
`timeout parameter in interval method should be positive number or zero. '${value}' was passed`, | ||
); | ||
} | ||
return createStore(value); | ||
} | ||
throw new TypeError( | ||
`timeout parameter in interval method should be number or Store. "${typeof value}" was passed`, | ||
); | ||
} |
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,146 @@ | ||
// @ts-nocheck | ||
import { createEvent, createStore } from 'effector'; | ||
import { argumentHistory, wait } from '../test-library'; | ||
import { interval } from './index'; | ||
|
||
test('after timeout tick triggered only once', async () => { | ||
const start = createEvent(); | ||
const stop = createEvent(); | ||
const { tick } = interval({ timeout: 10, start, stop }); | ||
const tickFn = watch(tick); | ||
|
||
expect(tickFn).not.toBeCalled(); | ||
|
||
start(); | ||
expect(tickFn).not.toBeCalled(); | ||
|
||
await wait(10); | ||
expect(tickFn).toBeCalledTimes(1); | ||
|
||
stop(); | ||
}); | ||
|
||
test('triggers tick multiple times', async () => { | ||
const start = createEvent(); | ||
const stop = createEvent(); | ||
const { tick } = interval({ timeout: 10, start, stop }); | ||
const tickFn = watch(tick); | ||
|
||
start(); | ||
await wait(10); | ||
expect(tickFn).toBeCalledTimes(1); | ||
|
||
await wait(10); | ||
expect(tickFn).toBeCalledTimes(2); | ||
|
||
await wait(10); | ||
expect(tickFn).toBeCalledTimes(3); | ||
|
||
stop(); | ||
await wait(20); | ||
expect(tickFn).toBeCalledTimes(3); | ||
}); | ||
|
||
test('after stop interval do not triggers again', async () => { | ||
const start = createEvent(); | ||
const stop = createEvent(); | ||
const { tick } = interval({ timeout: 10, start, stop }); | ||
const tickFn = watch(tick); | ||
|
||
start(); | ||
await wait(10); | ||
expect(tickFn).toBeCalledTimes(1); | ||
|
||
stop(); | ||
await wait(20); | ||
expect(tickFn).toBeCalledTimes(1); | ||
}); | ||
|
||
test('after timeout tick triggered only once', async () => { | ||
const start = createEvent(); | ||
const stop = createEvent(); | ||
const { tick } = interval({ timeout: 10, leading: true, start, stop }); | ||
const tickFn = watch(tick); | ||
|
||
expect(tickFn).not.toBeCalled(); | ||
|
||
start(); | ||
expect(tickFn).toBeCalled(); | ||
|
||
await wait(10); | ||
expect(tickFn).toBeCalledTimes(2); | ||
|
||
stop(); | ||
}); | ||
|
||
test('timeout can be changed during execution (timeout will be changed in the next tick)', async () => { | ||
const start = createEvent(); | ||
const stop = createEvent(); | ||
const increment = createEvent(); | ||
const $timeout = createStore(10).on(increment, (timeout) => timeout * 2); | ||
const { tick } = interval({ timeout: $timeout, start, stop }); | ||
const tickFn = watch(tick); | ||
|
||
start(); | ||
await wait(10); | ||
expect(tickFn).toBeCalledTimes(1); | ||
|
||
increment(); // timeout will be changed in the next tick | ||
await wait(10); | ||
expect(tickFn).toBeCalledTimes(2); | ||
|
||
await wait(10); // $timeout now is 20ms | ||
expect(tickFn).toBeCalledTimes(2); // not ticked yet | ||
|
||
await wait(10); | ||
expect(tickFn).toBeCalledTimes(3); // ticked | ||
|
||
stop(); | ||
}); | ||
|
||
test('opened should be true on pending interval', async () => { | ||
const start = createEvent(); | ||
const stop = createEvent(); | ||
const { opened } = interval({ timeout: 10, start, stop }); | ||
const openedFn = watch(opened); | ||
|
||
expect(argumentHistory(openedFn)).toMatchInlineSnapshot(` | ||
Array [ | ||
false, | ||
] | ||
`); | ||
|
||
start(); | ||
await wait(10); | ||
expect(argumentHistory(openedFn)).toMatchInlineSnapshot(` | ||
Array [ | ||
false, | ||
true, | ||
] | ||
`); | ||
|
||
await wait(20); | ||
expect(argumentHistory(openedFn)).toMatchInlineSnapshot(` | ||
Array [ | ||
false, | ||
true, | ||
] | ||
`); | ||
|
||
stop(); | ||
await wait(20); | ||
expect(argumentHistory(openedFn)).toMatchInlineSnapshot(` | ||
Array [ | ||
false, | ||
true, | ||
false, | ||
] | ||
`); | ||
}); | ||
|
||
/** Triggers fn on effect start */ | ||
function watch<T>(unit: Event<T> | Store<T> | Effect<T, any, any>) { | ||
const fn = jest.fn(); | ||
unit.watch(fn); | ||
return fn; | ||
} |