Skip to content

Commit

Permalink
feat(delay): Allow passing array of targets to delay (#288)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexander Khoroshikh <[email protected]>
  • Loading branch information
kireevmp and AlexandrHoroshih authored Oct 11, 2023
1 parent 6bce049 commit 0478526
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 29 deletions.
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
{
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>()],
}),
);
}

0 comments on commit 0478526

Please sign in to comment.