Skip to content

Commit 036b2f0

Browse files
authored
Merge pull request #333 from earthspacon/feat/spread-support-array-in-targets
spread - support array of units in target fields
2 parents 587d936 + 90ec10c commit 036b2f0

File tree

5 files changed

+648
-44
lines changed

5 files changed

+648
-44
lines changed

src/spread/index.ts

+44-37
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,71 @@
11
import {
22
createEvent,
33
EventCallable,
4-
is,
54
sample,
5+
Tuple,
66
Unit,
77
UnitTargetable,
88
} from 'effector';
99

10+
type TargetUnits<T> =
11+
| UnitTargetable<T | void>
12+
| Tuple<UnitTargetable<T | void>>
13+
| ReadonlyArray<UnitTargetable<T | void>>;
14+
1015
const hasPropBase = {}.hasOwnProperty;
1116
const hasOwnProp = <O extends { [k: string]: unknown }>(object: O, key: string) =>
1217
hasPropBase.call(object, key);
1318

14-
type NoInfer<T> = [T][T extends any ? 0 : never];
19+
/**
20+
* @example
21+
* spread({
22+
* source: dataObject,
23+
* targets: { first: targetA, second: [target1, target2] },
24+
* })
25+
*
26+
* sample({
27+
* source: dataObject,
28+
* target: spread({ targets: { first: targetA, second: [target1, target2] } })
29+
* })
30+
*
31+
* sample({
32+
* source: dataObject,
33+
* target: spread({ first: targetA, second: [target1, target2] })
34+
* })
35+
*/
1536

1637
export function spread<Payload>(config: {
1738
targets: {
18-
[Key in keyof Payload]?: UnitTargetable<Payload[Key]>;
39+
[Key in keyof Payload]?: TargetUnits<Payload[Key]>;
1940
};
2041
}): EventCallable<Partial<Payload>>;
2142

2243
export function spread<
2344
Source,
2445
Payload extends Source extends Unit<infer S> ? S : never,
25-
>(config: {
26-
source: Source;
27-
targets: {
28-
[Key in keyof Payload]?:
29-
| EventCallable<Partial<Payload[Key]>>
30-
| UnitTargetable<NoInfer<Payload[Key]>>;
31-
};
32-
}): Source;
46+
Targets extends {
47+
[Key in keyof Payload]?: Targets[Key] extends TargetUnits<infer TargetType>
48+
? Payload[Key] extends TargetType
49+
? TargetUnits<TargetType>
50+
: TargetUnits<Payload[Key]>
51+
: TargetUnits<Payload[Key]>;
52+
},
53+
>(config: { source: Source; targets: Targets }): Source;
3354

3455
export function spread<Payload>(targets: {
35-
[Key in keyof Payload]?: UnitTargetable<Payload[Key]>;
56+
[Key in keyof Payload]?: TargetUnits<Payload[Key]>;
3657
}): EventCallable<Partial<Payload>>;
3758

38-
/**
39-
* @example
40-
* spread({ source: dataObject, targets: { first: targetA, second: targetB } })
41-
* sample({
42-
* target: spread({targets: { first: targetA, second: targetB } })
43-
* })
44-
*/
4559
export function spread<P>(
4660
args:
4761
| {
4862
targets: {
49-
[Key in keyof P]?: Unit<P[Key]>;
63+
[Key in keyof P]?: TargetUnits<P[Key]>;
5064
};
5165
source?: Unit<P>;
5266
}
5367
| {
54-
[Key in keyof P]?: Unit<P[Key]>;
68+
[Key in keyof P]?: TargetUnits<P[Key]>;
5569
},
5670
): EventCallable<P> {
5771
const argsShape = isTargets(args) ? { targets: args } : args;
@@ -60,18 +74,14 @@ export function spread<P>(
6074
if (hasOwnProp(targets, targetKey)) {
6175
const currentTarget = targets[targetKey];
6276

63-
const hasTargetKey = sample({
64-
source,
65-
batch: false,
66-
filter: (object): object is any =>
67-
typeof object === 'object' && object !== null && targetKey in object,
68-
});
69-
7077
sample({
71-
batch: false,
72-
clock: hasTargetKey,
78+
source,
79+
filter: (object): object is any => {
80+
return typeof object === 'object' && object !== null && targetKey in object;
81+
},
7382
fn: (object: P) => object[targetKey],
7483
target: currentTarget as UnitTargetable<any>,
84+
batch: false,
7585
});
7686
}
7787
}
@@ -83,18 +93,15 @@ function isTargets<P>(
8393
args:
8494
| {
8595
targets: {
86-
[Key in keyof P]?: Unit<P[Key]>;
96+
[Key in keyof P]?: TargetUnits<P[Key]>;
8797
};
8898
source?: Unit<P>;
8999
}
90100
| {
91-
[Key in keyof P]?: Unit<P[Key]>;
101+
[Key in keyof P]?: TargetUnits<P[Key]>;
92102
},
93103
): args is {
94-
[Key in keyof P]?: Unit<P[Key]>;
104+
[Key in keyof P]?: TargetUnits<P[Key]>;
95105
} {
96-
return Object.keys(args).some(
97-
(key) =>
98-
!['targets', 'source'].includes(key) && is.unit(args[key as keyof typeof args]),
99-
);
106+
return !Object.keys(args).some((key) => ['targets', 'source'].includes(key));
100107
}

src/spread/readme.md

+58
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,61 @@ source = spread({ targets: { field: target, ... } })
216216
### Returns
217217

218218
- `source` `(Event<T>)` — Source event, data passed to it should be an object with fields from `targets`
219+
220+
## `source = spread({ targets: { field: Unit[] } })`
221+
222+
### Motivation
223+
224+
Multiple units can be passed for each target field
225+
226+
### Formulae
227+
228+
```ts
229+
source = spread({ field: [target1, target2], ... })
230+
231+
source = spread({ targets: { field: [target1, target2], ... } })
232+
233+
spread({ source, targets: { field: [target1, target2], ... } })
234+
```
235+
236+
- When `source` is triggered with **object**, extract `field` from data, and trigger all targets of `target`
237+
- `targets` can have multiple properties with multiple units
238+
- If the `source` was triggered with non-object, nothing would be happening
239+
- If `source` is triggered with object but without property `field`, no unit of the target for this `field` will be triggered
240+
241+
### Example
242+
243+
#### Trigger multiple units for each field of payload
244+
245+
```ts
246+
const roomEntered = createEvent<{
247+
roomId: string;
248+
userId: string;
249+
message: string;
250+
}>();
251+
const userIdChanged = createEvent<string>();
252+
253+
const $roomMessage = createStore('');
254+
const $currentRoomId = createStore<string | null>(null);
255+
256+
const getRoomFx = createEffect((roomId: string) => roomId);
257+
const setUserIdFx = createEffect((userId: string) => userId);
258+
259+
sample({
260+
clock: roomEntered,
261+
target: spread({
262+
roomId: [getRoomFx, $currentRoomId],
263+
userId: [setUserIdFx, userIdChanged],
264+
message: $roomMessage,
265+
}),
266+
});
267+
268+
roomEntered({
269+
roomId: 'roomId',
270+
userId: 'userId',
271+
message: 'message',
272+
});
273+
// => getRoomFx('roomId'), update $currentRoomId with 'roomId'
274+
// => setUserIdFx('userId'), userIdChanged('userId')
275+
// => update $roomMessage with 'message'
276+
```

src/spread/spread.fork.test.ts

+114
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,117 @@ test('do not affects another scope', async () => {
9090
}
9191
`);
9292
});
93+
94+
describe('targets: array of units', () => {
95+
test('works in forked scope', async () => {
96+
const app = createDomain();
97+
const source = app.createEvent<{
98+
first: string;
99+
second: number;
100+
third: string;
101+
}>();
102+
const first = app.createEvent<string>();
103+
const second = app.createEvent<number>();
104+
105+
const $thirdA = app.createStore('');
106+
const $thirdB = app.createStore('');
107+
108+
const $first = app.createStore('').on(first, (_, p) => p);
109+
const $second = restore(second, 0);
110+
111+
spread({
112+
source,
113+
targets: { first, second, third: [$thirdA, $thirdB] },
114+
});
115+
116+
const scope = fork();
117+
118+
await allSettled(source, {
119+
scope,
120+
params: { first: 'sergey', second: 26, third: '30' },
121+
});
122+
123+
expect(scope.getState($first)).toBe('sergey');
124+
expect(scope.getState($second)).toBe(26);
125+
expect(scope.getState($thirdA)).toBe('30');
126+
expect(scope.getState($thirdB)).toBe('30');
127+
});
128+
129+
test('does not affect original store state', async () => {
130+
const app = createDomain();
131+
const source = app.createEvent<{
132+
first: string;
133+
second: number;
134+
third: string;
135+
}>();
136+
const first = app.createEvent<string>();
137+
const second = app.createEvent<number>();
138+
139+
const $thirdA = app.createStore('');
140+
const $thirdB = app.createStore('');
141+
142+
const $first = app.createStore('').on(first, (_, p) => p);
143+
const $second = restore(second, 0);
144+
145+
spread({
146+
source,
147+
targets: { first, second, third: [$thirdA, $thirdB] },
148+
});
149+
150+
const scope = fork();
151+
152+
await allSettled(source, {
153+
scope,
154+
params: { first: 'sergey', second: 26, third: '30' },
155+
});
156+
157+
expect(scope.getState($first)).toBe('sergey');
158+
expect(scope.getState($second)).toBe(26);
159+
expect(scope.getState($thirdA)).toBe('30');
160+
expect(scope.getState($thirdB)).toBe('30');
161+
162+
expect($first.getState()).toBe('');
163+
expect($second.getState()).toBe(0);
164+
expect($thirdA.getState()).toBe('');
165+
expect($thirdB.getState()).toBe('');
166+
});
167+
168+
test('do not affects another scope', async () => {
169+
const app = createDomain();
170+
const source = app.createEvent<{
171+
first: string;
172+
second: number;
173+
third: string;
174+
}>();
175+
const first = app.createEvent<string>();
176+
const second = app.createEvent<number>();
177+
178+
const $thirdA = app.createStore('');
179+
const $thirdB = app.createStore('');
180+
181+
const $first = app.createStore('').on(first, (_, p) => p);
182+
const $second = restore(second, 0);
183+
184+
spread({
185+
source,
186+
targets: { first, second, third: [$thirdA, $thirdB] },
187+
});
188+
189+
const scope1 = fork();
190+
const scope2 = fork();
191+
192+
await Promise.all([
193+
allSettled(source, {
194+
scope: scope1,
195+
params: { first: 'sergey', second: 26, third: '30' },
196+
}),
197+
allSettled(source, {
198+
scope: scope2,
199+
params: { first: 'Anon', second: 90, third: '154' },
200+
}),
201+
]);
202+
203+
expect(scope1.getState($first)).toBe('sergey');
204+
expect(scope1.getState($second)).toBe(26);
205+
});
206+
});

0 commit comments

Comments
 (0)