Skip to content

Commit

Permalink
feat(interval): implement basic interval
Browse files Browse the repository at this point in the history
  • Loading branch information
sergeysova committed Jul 27, 2021
1 parent 97e47ef commit 503bc43
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 0 deletions.
9 changes: 9 additions & 0 deletions interval/index.d.ts
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> };
75 changes: 75 additions & 0 deletions interval/index.js
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`,
);
}
146 changes: 146 additions & 0 deletions interval/interval.test.ts
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;
}

0 comments on commit 503bc43

Please sign in to comment.