Skip to content

Commit

Permalink
feat: expose timers api
Browse files Browse the repository at this point in the history
  • Loading branch information
movpushmov committed Apr 22, 2024
1 parent aa7b1eb commit 520a790
Show file tree
Hide file tree
Showing 16 changed files with 470 additions and 85 deletions.
43 changes: 40 additions & 3 deletions src/debounce/debounce.fork.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -232,3 +232,40 @@ describe('edge cases', () => {
expect(triggerListener).toBeCalledTimes(0);
})
});

test('exposed timers api', async () => {
const timerFx = createEffect(({ timeoutId, rejectPromise, saveCancel, timeout }: DebounceTimerFxProps) => {
if (timeoutId) clearTimeout(timeoutId);
if (rejectPromise) rejectPromise();
return new Promise((resolve, reject) => {
saveCancel([setTimeout(resolve, timeout / 2), 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();
});
57 changes: 39 additions & 18 deletions src/debounce/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,34 @@ import {
merge,
UnitTargetable,
EventAsReturnType,
} from 'effector';
createEffect, EventCallable
} from 'effector'

export type DebounceTimerFxProps = {
timeoutId?: NodeJS.Timeout;
rejectPromise?: () => void;
saveCancel: EventCallable<[NodeJS.Timeout, () => void]>;
timeout: number;
};

const timerFx = createEffect(({ timeoutId, rejectPromise, saveCancel, timeout }: DebounceTimerFxProps) => {
if (timeoutId) clearTimeout(timeoutId);
if (rejectPromise) rejectPromise();
return new Promise((resolve, reject) => {
saveCancel([setTimeout(resolve, timeout), reject]);
});
});

export function debounce<T>(
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>,
>(_: {
Expand All @@ -29,7 +45,7 @@ export function debounce<
target: Target;
name?: string;
}): Target;
export function debounce<T>(
export function _debounce<T>(
...args:
| [
{
Expand Down Expand Up @@ -57,18 +73,19 @@ export function debounce<T>(

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]);
});
},
mapParams: (timeout: number, [timeoutId, rejectPromise]) => ({
timeout,
timeoutId,
rejectPromise,
saveCancel
}),
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(
Expand All @@ -88,15 +105,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({
Expand All @@ -108,19 +125,23 @@ export function debounce<T>(
sample({
source: $timeout,
clock: triggerTick,
target: timerFx,
target: innerTimerFx,
});

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') {
Expand Down
22 changes: 22 additions & 0 deletions src/debounce/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,25 @@ someHappened(4);

// someHappened now 4
```

### [Tests] Exposed timers API example

```ts
const timerFx = createEffect(({ timeoutId, rejectPromise, saveCancel, timeout }: DebounceTimerFxProps) => {
if (timeoutId) myClearTimeout(timeoutId);
if (rejectPromise) rejectPromise();
return new Promise((resolve, reject) => {
saveCancel([mySetTimeout(resolve, timeout), reject]);
});
});

const scope = fork({
handlers: [[debounce.timerFx, timerFx]],
});

const clock = createEvent();
const tick = debounce(clock, 200);

// important! call from scope
allSettled(clock, { scope });
```
45 changes: 43 additions & 2 deletions src/delay/delay.fork.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import 'regenerator-runtime/runtime';
import { createDomain, fork, serialize, allSettled } from 'effector';
import {
createDomain,
fork,
serialize,
allSettled,
createEffect, createEvent, createWatch, UnitValue
} from 'effector'

import { delay } from './index';
import { delay, DelayTimerFxProps } from './index'
import { wait } from '../../test-library'

test('throttle works in forked scope', async () => {
const app = createDomain();
Expand Down Expand Up @@ -127,3 +134,37 @@ test('throttle do not affect original store value', async () => {

expect($counter.getState()).toMatchInlineSnapshot(`0`);
});

test('exposed timers api', async () => {
const timerFx = createEffect<DelayTimerFxProps, UnitValue<any>>(
({ payload, milliseconds }) =>
new Promise((resolve) => {
setTimeout(resolve, milliseconds / 2, payload);
}),
)

const scope = fork({
handlers: [[delay.timerFx, timerFx]],
});

const mockedFn = jest.fn();

const clock = createEvent();
const tick = delay(clock, 50);

createWatch({
unit: tick,
fn: mockedFn,
scope,
});

allSettled(clock, { scope });

await wait(20);

expect(mockedFn).not.toBeCalled();

await wait(5);

expect(mockedFn).toBeCalled();
});
37 changes: 22 additions & 15 deletions src/delay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,36 @@ import {
MultiTarget,
UnitValue,
UnitTargetable,
attach,
} from 'effector';

type TimeoutType<Payload> = ((payload: Payload) => number) | Store<number> | number;
export type DelayTimerFxProps = { payload: UnitValue<any>; milliseconds: number };

export function delay<Source extends Unit<any>>(
const timerFx = createEffect<DelayTimerFxProps, UnitValue<any>>(
({ payload, milliseconds }) =>
new Promise((resolve) => {
setTimeout(resolve, milliseconds, payload);
}),
)

export function _delay<Source extends Unit<any>>(
source: Source,
timeout: TimeoutType<UnitValue<Source>>,
): EventAsReturnType<UnitValue<Source>>;

export function delay<Source extends Unit<any>, Target extends TargetType>(config: {
export function _delay<Source extends Unit<any>, Target extends TargetType>(config: {
source: Source;
timeout: TimeoutType<UnitValue<Source>>;
target: MultiTarget<Target, UnitValue<Source>>;
}): Target;

export function delay<Source extends Unit<any>>(config: {
export function _delay<Source extends Unit<any>>(config: {
source: Source;
timeout: TimeoutType<UnitValue<Source>>;
}): EventAsReturnType<UnitValue<Source>>;

export function delay<
export function _delay<
Source extends Unit<any>,
Target extends TargetType = TargetType,
>(
Expand All @@ -57,15 +66,9 @@ export function delay<

const ms = validateTimeout(timeout);

const timerFx = createEffect<
{ payload: UnitValue<Source>; milliseconds: number },
UnitValue<Source>
>(
({ payload, milliseconds }) =>
new Promise((resolve) => {
setTimeout(resolve, milliseconds, payload);
}),
);
const innerTimerFx = attach({
effect: timerFx
});

sample({
// ms can be Store<number> | number
Expand All @@ -77,14 +80,18 @@ export function delay<
milliseconds:
typeof milliseconds === 'function' ? milliseconds(payload) : milliseconds,
}),
target: timerFx,
target: innerTimerFx,
});

sample({ clock: timerFx.doneData, target: targets as UnitTargetable<any>[] });
sample({ clock: innerTimerFx.doneData, target: targets as UnitTargetable<any>[] });

return target as any;
}

export const delay = Object.assign(_delay, {
timerFx
});

function validateTimeout<T>(
timeout: number | ((_: T) => number) | Store<number> | unknown,
) {
Expand Down
21 changes: 21 additions & 0 deletions src/delay/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,24 @@ update('Hello');
// after 500ms
// => log Hello
```

### [Tests] Exposed timers API example

```ts
const timerFx = createEffect<DelayTimerFxProps, UnitValue<any>>(
({ payload, milliseconds }) =>
new Promise((resolve) => {
mySetTimeout(resolve, milliseconds, payload);
}),
)

const scope = fork({
handlers: [[delay.timerFx, timerFx]],
});

const clock = createEvent();
const tick = delay(clock, 200);

// important! call from scope
allSettled(clock, { scope });
```
Loading

0 comments on commit 520a790

Please sign in to comment.