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 to pass array of units for spread operator #333

Merged
81 changes: 44 additions & 37 deletions src/spread/index.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,71 @@
import {
createEvent,
EventCallable,
is,
sample,
Tuple,
Unit,
UnitTargetable,
} from 'effector';

type TargetUnits<T> =
| UnitTargetable<T | void>
| Tuple<UnitTargetable<T | void>>
| ReadonlyArray<UnitTargetable<T | void>>;

const hasPropBase = {}.hasOwnProperty;
const hasOwnProp = <O extends { [k: string]: unknown }>(object: O, key: string) =>
hasPropBase.call(object, key);

type NoInfer<T> = [T][T extends any ? 0 : never];
/**
* @example
* spread({
* source: dataObject,
* targets: { first: targetA, second: [target1, target2] },
* })
*
* sample({
* source: dataObject,
* target: spread({ targets: { first: targetA, second: [target1, target2] } })
* })
*
* sample({
* source: dataObject,
* target: spread({ first: targetA, second: [target1, target2] })
* })
*/

export function spread<Payload>(config: {
targets: {
[Key in keyof Payload]?: UnitTargetable<Payload[Key]>;
[Key in keyof Payload]?: TargetUnits<Payload[Key]>;
};
}): EventCallable<Partial<Payload>>;

export function spread<
Source,
Payload extends Source extends Unit<infer S> ? S : never,
>(config: {
source: Source;
targets: {
[Key in keyof Payload]?:
| EventCallable<Partial<Payload[Key]>>
| UnitTargetable<NoInfer<Payload[Key]>>;
};
}): Source;
Targets extends {
[Key in keyof Payload]?: Targets[Key] extends TargetUnits<infer TargetType>
? Payload[Key] extends TargetType
? TargetUnits<TargetType>
: TargetUnits<Payload[Key]>
: TargetUnits<Payload[Key]>;
},
>(config: { source: Source; targets: Targets }): Source;

export function spread<Payload>(targets: {
[Key in keyof Payload]?: UnitTargetable<Payload[Key]>;
[Key in keyof Payload]?: TargetUnits<Payload[Key]>;
}): EventCallable<Partial<Payload>>;

/**
* @example
* spread({ source: dataObject, targets: { first: targetA, second: targetB } })
* sample({
* target: spread({targets: { first: targetA, second: targetB } })
* })
*/
export function spread<P>(
args:
| {
targets: {
[Key in keyof P]?: Unit<P[Key]>;
[Key in keyof P]?: TargetUnits<P[Key]>;
};
source?: Unit<P>;
}
| {
[Key in keyof P]?: Unit<P[Key]>;
[Key in keyof P]?: TargetUnits<P[Key]>;
},
): EventCallable<P> {
const argsShape = isTargets(args) ? { targets: args } : args;
Expand All @@ -60,18 +74,14 @@ export function spread<P>(
if (hasOwnProp(targets, targetKey)) {
const currentTarget = targets[targetKey];

const hasTargetKey = sample({
source,
batch: false,
filter: (object): object is any =>
typeof object === 'object' && object !== null && targetKey in object,
});

sample({
batch: false,
clock: hasTargetKey,
source,
filter: (object): object is any => {
return typeof object === 'object' && object !== null && targetKey in object;
},
fn: (object: P) => object[targetKey],
target: currentTarget as UnitTargetable<any>,
batch: false,
});
}
}
Expand All @@ -83,18 +93,15 @@ function isTargets<P>(
args:
| {
targets: {
[Key in keyof P]?: Unit<P[Key]>;
[Key in keyof P]?: TargetUnits<P[Key]>;
};
source?: Unit<P>;
}
| {
[Key in keyof P]?: Unit<P[Key]>;
[Key in keyof P]?: TargetUnits<P[Key]>;
},
): args is {
[Key in keyof P]?: Unit<P[Key]>;
[Key in keyof P]?: TargetUnits<P[Key]>;
} {
return Object.keys(args).some(
(key) =>
!['targets', 'source'].includes(key) && is.unit(args[key as keyof typeof args]),
);
return !Object.keys(args).some((key) => ['targets', 'source'].includes(key));
}
58 changes: 58 additions & 0 deletions src/spread/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,61 @@ source = spread({ targets: { field: target, ... } })
### Returns

- `source` `(Event<T>)` — Source event, data passed to it should be an object with fields from `targets`

## `source = spread({ targets: { field: Unit[] } })`

### Motivation

Multiple units can be passed for each target field

### Formulae

```ts
source = spread({ field: [target1, target2], ... })

source = spread({ targets: { field: [target1, target2], ... } })

spread({ source, targets: { field: [target1, target2], ... } })
```

- When `source` is triggered with **object**, extract `field` from data, and trigger all targets of `target`
- `targets` can have multiple properties with multiple units
- If the `source` was triggered with non-object, nothing would be happening
- If `source` is triggered with object but without property `field`, no unit of the target for this `field` will be triggered

### Example

#### Trigger multiple units for each field of payload

```ts
const roomEntered = createEvent<{
roomId: string;
userId: string;
message: string;
}>();
const userIdChanged = createEvent<string>();

const $roomMessage = createStore('');
const $currentRoomId = createStore<string | null>(null);

const getRoomFx = createEffect((roomId: string) => roomId);
const setUserIdFx = createEffect((userId: string) => userId);

sample({
clock: roomEntered,
target: spread({
roomId: [getRoomFx, $currentRoomId],
userId: [setUserIdFx, userIdChanged],
message: $roomMessage,
}),
});

roomEntered({
roomId: 'roomId',
userId: 'userId',
message: 'message',
});
// => getRoomFx('roomId'), update $currentRoomId with 'roomId'
// => setUserIdFx('userId'), userIdChanged('userId')
// => update $roomMessage with 'message'
```
114 changes: 114 additions & 0 deletions src/spread/spread.fork.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,117 @@ test('do not affects another scope', async () => {
}
`);
});

describe('targets: array of units', () => {
test('works in forked scope', async () => {
const app = createDomain();
const source = app.createEvent<{
first: string;
second: number;
third: string;
}>();
const first = app.createEvent<string>();
const second = app.createEvent<number>();

const $thirdA = app.createStore('');
const $thirdB = app.createStore('');

const $first = app.createStore('').on(first, (_, p) => p);
const $second = restore(second, 0);

spread({
source,
targets: { first, second, third: [$thirdA, $thirdB] },
});

const scope = fork();

await allSettled(source, {
scope,
params: { first: 'sergey', second: 26, third: '30' },
});

expect(scope.getState($first)).toBe('sergey');
expect(scope.getState($second)).toBe(26);
expect(scope.getState($thirdA)).toBe('30');
expect(scope.getState($thirdB)).toBe('30');
});

test('does not affect original store state', async () => {
const app = createDomain();
const source = app.createEvent<{
first: string;
second: number;
third: string;
}>();
const first = app.createEvent<string>();
const second = app.createEvent<number>();

const $thirdA = app.createStore('');
const $thirdB = app.createStore('');

const $first = app.createStore('').on(first, (_, p) => p);
const $second = restore(second, 0);

spread({
source,
targets: { first, second, third: [$thirdA, $thirdB] },
});

const scope = fork();

await allSettled(source, {
scope,
params: { first: 'sergey', second: 26, third: '30' },
});

expect(scope.getState($first)).toBe('sergey');
expect(scope.getState($second)).toBe(26);
expect(scope.getState($thirdA)).toBe('30');
expect(scope.getState($thirdB)).toBe('30');

expect($first.getState()).toBe('');
expect($second.getState()).toBe(0);
expect($thirdA.getState()).toBe('');
expect($thirdB.getState()).toBe('');
});

test('do not affects another scope', async () => {
const app = createDomain();
const source = app.createEvent<{
first: string;
second: number;
third: string;
}>();
const first = app.createEvent<string>();
const second = app.createEvent<number>();

const $thirdA = app.createStore('');
const $thirdB = app.createStore('');

const $first = app.createStore('').on(first, (_, p) => p);
const $second = restore(second, 0);

spread({
source,
targets: { first, second, third: [$thirdA, $thirdB] },
});

const scope1 = fork();
const scope2 = fork();

await Promise.all([
allSettled(source, {
scope: scope1,
params: { first: 'sergey', second: 26, third: '30' },
}),
allSettled(source, {
scope: scope2,
params: { first: 'Anon', second: 90, third: '154' },
}),
]);

expect(scope1.getState($first)).toBe('sergey');
expect(scope1.getState($second)).toBe(26);
});
});
Loading
Loading