Skip to content
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

Allow passing array of targets to delay #288

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/delay/delay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,49 @@ test('double delay effect', async () => {
]
`);
});

test('delay with array of units', async () => {
expect.assertions(2);

const source = createEvent();

const fnA = jest.fn();
const targetA = createEvent();

targetA.watch(fnA);

const fnB = jest.fn();
const targetB = createEvent();

targetB.watch(fnB);

delay({ source, timeout: 100, target: [targetA, targetB] });

source(1);

await waitFor(targetA);

expect(fnA).toHaveBeenCalledTimes(1);
expect(fnB).toHaveBeenCalledTimes(1);
});

test('delay throws when any of targets is not a unit', async () => {
expect.assertions(1);

const source = createEvent();
const target = createEvent();

expect(() =>
delay({ source, timeout: 100, target: [target, 'not a unit'] }),
).toThrowError(/target must be a unit/);
});

test('delay throws when source is not a unit', async () => {
expect.assertions(1);

const target = createEvent();

expect(() => delay({ source: 'not a unit', timeout: 100, target })).toThrowError(
/source must be a unit/,
);
});
58 changes: 36 additions & 22 deletions src/delay/index.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,54 @@
import {
createEffect,
createEvent,
forward,
is,
sample,
Unit,
Event,
Store,
Effect,
EventAsReturnType,
combine,
Target as TargetType,
MultiTarget,
UnitValue,
} from 'effector';

type EventAsReturnType<Payload> = any extends Payload ? Event<Payload> : never;
type TimeoutType<Payload> = ((payload: Payload) => number) | Store<number> | number;

export function delay<T>({
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: {
source: Source;
timeout: TimeoutType<UnitValue<Source>>;
}): EventAsReturnType<UnitValue<Source>>;

export function delay<
Source extends Unit<any>,
Target extends TargetType = TargetType,
>({
source,
timeout,
target = createEvent<T>(),
target = createEvent() as any,
}: {
source: Unit<T>;
timeout: ((_payload: T) => number) | Store<number> | number;
target?:
| Store<T>
| Event<T>
| Effect<T, any, any>
| Event<void>
| Effect<void, any, any>;
}): EventAsReturnType<T> {
if (!is.unit(source)) throw new TypeError('source must be a unit from effector');
source: Source;
timeout: TimeoutType<UnitValue<Source>>;
target?: MultiTarget<Target, UnitValue<Source>>;
}): typeof target extends undefined ? EventAsReturnType<UnitValue<Source>> : Target {
const targets = Array.isArray(target) ? target : [target];

if (!is.unit(target)) throw new TypeError('target must be a unit from effector');
if (!is.unit(source)) throw new TypeError('source must be a unit from effector');
if (!targets.every((unit) => is.unit(unit)))
throw new TypeError('target must be a unit from effector');

const ms = validateTimeout(timeout);

const timerFx = createEffect<{ payload: T; milliseconds: number }, T>(
const timerFx = createEffect<
{ payload: UnitValue<Source>; milliseconds: number },
UnitValue<Source>
>(
({ payload, milliseconds }) =>
new Promise((resolve) => {
setTimeout(resolve, milliseconds, payload);
Expand All @@ -44,7 +59,7 @@ export function delay<T>({
// ms can be Store<number> | number
// converts object of stores or object of values to store
source: combine({ milliseconds: ms }),
clock: source,
clock: source as Unit<any>,
fn: ({ milliseconds }, payload) => ({
payload,
milliseconds:
Expand All @@ -53,10 +68,9 @@ export function delay<T>({
target: timerFx,
});

// @ts-expect-error
forward({ from: timerFx.doneData, to: target });
sample({ clock: timerFx.doneData, target: targets as Unit<any>[] });

return timerFx.doneData;
return target as any;
}

function validateTimeout<T>(
Expand Down
16 changes: 12 additions & 4 deletions src/delay/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ target = delay({ source, timeout: number, target });

1. `source` `(Event<T>` | `Store<T>` | `Effect<T>)` — Source unit, data from this unit used to trigger `target` with.
1. `timeout` `(number)` — time to wait before trigger `event`
1. `target` `(Event<T>` | `Store<T>` | `Effect<T`>)` — Optional. Target unit, that should be called after delay.
1. `target` `(Unit<T>` | `Array<Unit<T>>)` — Optional. Target unit or array of units that will be called after delay.

### Returns

- `event` `(Event<T>)` — New event, that triggered after delay
- `target` `(Unit<T>` | `Array<Unit<T>>)` — Target unit or units that were passed to `delay`

### Example

Expand Down Expand Up @@ -61,7 +61,11 @@ target = delay({ source, timeout: Function, target });

1. `source` `(Event<T>` | `Store<T>` | `Effect<T>)` — Source unit, data from this unit used to trigger `target` with.
1. `timeout` `((payload: T) => number)` — Calculate delay for each `source` call. Receives the payload of `source` as argument. Should return `number` — delay in milliseconds.
1. `target` `(Event<T>` | `Store<T>` | `Effect<T`>)` — Optional. Target unit, that should be called after delay.
1. `target` `(Unit<T>` | `Array<Unit<T>>)` — Optional. Target unit or array of units that will be called after delay.

### Returns

- `target` `(Unit<T>` | `Array<Unit<T>>)` — Target unit or units that were passed to `delay`

### Example

Expand Down Expand Up @@ -109,7 +113,11 @@ target = delay({ source, timeout: $store, target });

1. `source` `(Event<T>` | `Store<T>` | `Effect<T>)` — Source unit, data from this unit used to trigger `target` with.
1. `timeout` `(Store<number>)` — Store with number — delay in milliseconds.
1. `target` `(Event<T>` | `Store<T>` | `Effect<T`>)` — Optional. Target unit, that should be called after delay.
1. `target` `(Unit<T>` | `Array<Unit<T>>)` — Optional. Target unit or array of units that will be called after delay.

### Returns

- `target` `(Unit<T>` | `Array<Unit<T>>)` — Target unit or units that were passed to `delay`

### Example

Expand Down
131 changes: 128 additions & 3 deletions test-typings/delay.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expectType } from 'tsd';
import { Event, createStore, createEvent, createEffect } from 'effector';
import { Event, createStore, createEvent, createEffect, Store } from 'effector';
import { delay } from '../src/delay';

// Check valid type for source
Expand Down Expand Up @@ -90,13 +90,138 @@ import { delay } from '../src/delay';
const source = createEvent<number>();
const target = createEvent();

expectType<Event<number>>(delay({ source, timeout: 100, target }));
expectType<typeof target>(delay({ source, timeout: 100, target }));
}

// void effects support
{
const source = createEvent<number>();
const target = createEffect<void, void>();

expectType<Event<number>>(delay({ source, timeout: 100, target }));
expectType<typeof target>(delay({ source, timeout: 100, target }));
}

// supports wider type in target
{
const source = createEvent<number>();
const target = createEvent<number | string>();

delay({ source, timeout: 100, target });
}

// does not allow narrower type in target
{
const source = createEvent<number>();
const target = createEvent<1 | 2>();

delay({
source,
timeout: 100,
// @ts-expect-error
target,
});
}

// supports multiple targets as an array
{
AlexandrHoroshih marked this conversation as resolved.
Show resolved Hide resolved
const source = createStore<string>('');

const $targetStore = createStore<string>('');

const targetEvent = createEvent<string>();
const targetEventVoid = createEvent<void>();

const targetEffect = createEffect<string, void>();
const targetEffectVoid = createEffect<void, void>();

delay({
source,
timeout: 100,
target: [
$targetStore,
targetEvent,
targetEventVoid,
targetEffect,
targetEffectVoid,
],
});
}

// does not allow invalid targets in array
{
const source = createStore<string>('');

// @ts-expect-error
delay({ source, timeout: 100, target: ['non-unit'] });
// @ts-expect-error
delay({ source, timeout: 100, target: [null] });
// @ts-expect-error
delay({ source, timeout: 100, target: [100] });
// @ts-expect-error
delay({ source, timeout: 100, target: [() => ''] });
}

// does not allow incompatible targets in array
{
const source = createStore<string>('');

// @ts-expect-error
delay({
source,
timeout: 100,
target: [createEvent<number>()],
});

// @ts-expect-error
delay({
source,
timeout: 100,
target: [createEffect<number, void>()],
});

// @ts-expect-error
delay({
source,
timeout: 100,
target: [createStore<number>(0)],
});

delay({
source,
timeout: 100,
// @ts-expect-error
target: [
createEvent<number>(),
createEffect<number, void>(),
createStore<number>(0),
],
});

// @ts-expect-error
delay({
source,
timeout: 100,
target: [createEvent<string>(), 'non-unit'],
});
}

// returns typeof target
{
const source = createStore<'x'>('x');

expectType<Event<'x'>>(
delay({
source,
timeout: 100,
target: createEvent<'x'>(),
}),
);

expectType<[Event<'x'>, Store<string>, Event<string>]>(
delay({
source,
timeout: 100,
target: [createEvent<'x'>(), createStore<string>(''), createEvent<string>()],
}),
);
}
Loading