Skip to content

Commit

Permalink
Merge pull request #333 from earthspacon/feat/spread-support-array-in…
Browse files Browse the repository at this point in the history
…-targets

spread - support array of units in target fields
  • Loading branch information
sergeysova authored Sep 30, 2024
2 parents 587d936 + 90ec10c commit 036b2f0
Show file tree
Hide file tree
Showing 5 changed files with 648 additions and 44 deletions.
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

0 comments on commit 036b2f0

Please sign in to comment.