From 2093bc14aea68e803e1634a476c2bd29adfd6c0f Mon Sep 17 00:00:00 2001 From: earthspacon Date: Thu, 1 Aug 2024 14:16:38 +0500 Subject: [PATCH 1/4] feat(splitMap): added targets field --- src/split-map/index.ts | 49 ++++++- src/split-map/readme.md | 111 ++++++++++++++ src/split-map/split-map.fork.test.ts | 68 +++++---- src/split-map/split-map.test.ts | 89 +++++++++++- test-typings/split-map.ts | 207 +++++++++++++++++++++++++++ 5 files changed, 490 insertions(+), 34 deletions(-) diff --git a/src/split-map/index.ts b/src/split-map/index.ts index 32bf9818..ff0a2868 100644 --- a/src/split-map/index.ts +++ b/src/split-map/index.ts @@ -1,36 +1,77 @@ -import { Event, is, Store, Unit } from 'effector'; +import { Event, Tuple, Unit, UnitTargetable, is, sample } from 'effector'; + +type TargetUnits = + | UnitTargetable + | Tuple> + | ReadonlyArray>; + +const hasPropBase = {}.hasOwnProperty; +const hasOwnProp = (object: O, key: string) => + hasPropBase.call(object, key); export function splitMap< S, Cases extends Record any | undefined>, + Targets extends { + [K in keyof Cases]?: Cases[K] extends (s: S) => infer R + ? Targets[K] extends TargetUnits + ? Exclude extends TargetType + ? TargetUnits + : TargetUnits> + : TargetUnits> + : never; + } & { __?: TargetUnits }, >({ source, cases, + targets, }: { source: Unit; cases: Cases; + targets?: Targets; }): { [K in keyof Cases]: Cases[K] extends (p: S) => infer R ? Event> : never; } & { __: Event } { - const result: Record | Store> = {}; + const result: Record> = {}; let current = is.store(source) ? source.updates : (source as Event); for (const key in cases) { - if (key in cases) { + if (hasOwnProp(cases, key)) { const fn = cases[key]; - result[key] = current.filterMap(fn); + const caseEvent = current.filterMap(fn); + + result[key] = caseEvent; + current = current.filter({ fn: (data) => !fn(data), }); + + if (targets && hasOwnProp(targets, key)) { + const currentTarget = targets[key]; + + sample({ + clock: caseEvent, + target: currentTarget as UnitTargetable, + }); + } } } // eslint-disable-next-line no-underscore-dangle result.__ = current; + if (targets && '__' in targets) { + const defaultCaseTarget = targets.__; + + sample({ + clock: current, + target: defaultCaseTarget as UnitTargetable, + }); + } + return result as any; } diff --git a/src/split-map/readme.md b/src/split-map/readme.md index bca56c09..e9f7c949 100644 --- a/src/split-map/readme.md +++ b/src/split-map/readme.md @@ -112,3 +112,114 @@ websocketEventReceived({ type: 'increment', name: 'demo', count: 5 }); websocketEventReceived({ type: 'bang', random: 'unknown' }); // => Unknown type: 'bang' ``` + +## `splitMap({ source, cases, targets? })` + +### Motivation + +Often we need the same behavior as in split — react to derived events received from `cases` and trigger the corresponding units. In `targets` field we can pass object with the same keys as in `cases` and values with `units` to trigger on the corresponding event. + +### Formulae + +```ts +splitMap({ source, cases, targets }); +``` + +- On each `source` trigger, call each function in `cases` object one after another until function returns non undefined, and call event in `shape` with the same name as function in `cases` object. +- Trigger the corresponding `unit`(s) from `targets` object +- If no function returned value, event `__` in `shape` should be triggered, same for `targets` field — trigger `unit`(s) provided in `__` key + +### Arguments + +1. `source` ( | | ) — Source unit, data from this unit passed to each function in `cases` object and `__` event in `shape` as is +2. `cases` (`{ [key: string]: (payload: T) => any | void }`) — Object of functions. Function receives one argument is a payload from `source`, should return any value or `undefined` +3. `targets` (`{ [key: string]?: Unit | Unit[]; __?: Unit | Unit[] }`) — Object of units to trigger on corresponding event from `cases` object + +### Returns + +- `shape` (`{ [key: string]: Event; __: Event }`) — Object of events, with the same structure as `cases`, but with the _default_ event `__`, that triggered when each other function returns `undefined` + +### Examples + +#### Split WebSocket events to effector events with targets + +```ts +import { createEvent } from 'effector'; +import { splitMap } from 'patronum/split-map'; +import { spread } from 'patronum/spread'; + +type WSEvent = + | { type: 'init'; key: string } + | { type: 'increment'; count: number } + | { type: 'reset' }; + +export const websocketEventReceived = createEvent(); + +const $isInitialized = createStore(false); +const $count = createStore(null); + +const getInitialDataFx = createEffect(); + +splitMap({ + source: websocketEventReceived, + cases: { + init: (event) => { + if (event.type === 'init') return { init: true, key: event.key }; + }, + increment: (payload) => { + if (payload.type === 'increment') return payload.count; + }, + reset: ({ type }) => { + if (type === 'reset') return null; + }, + }, + targets: { + // Event<{ init?: boolean; key?: string }> + init: spread({ init: $isInitialized, key: getInitialDataFx }), + increment: $count, + reset: [$count.reinit, $isInitialized.reinit], + }, +}); + +websocketEventReceived({ type: 'init', key: 'key' }); +// => $isInitialized: true, getInitialDataFx: 'key' + +websocketEventReceived({ type: 'increment', count: 2 }); +// => $count: 2 + +websocketEventReceived({ type: 'reset' }); +// => $count: null, $isInitialized: false +``` + +#### We can still use returned events to do some other logic + +```ts +const { init } = splitMap({ + source: websocketEventReceived, + cases: { + init: (event) => { + if (event.type === 'init') return { init: true, key: event.key }; + }, + increment: (payload) => { + if (payload.type === 'increment') return payload.count; + }, + reset: ({ type }) => { + if (type === 'reset') return null; + }, + }, + targets: { + // Event<{ init?: boolean; key?: string }> + init: spread({ init: $isInitialized, key: getInitialDataFx }), + increment: $count, + reset: [$count.reinit, $isInitialized.reinit], + }, +}); + +sample({ + clock: init, + // some other logic + source: ..., + filter: ..., + target: ..., +}) +``` diff --git a/src/split-map/split-map.fork.test.ts b/src/split-map/split-map.fork.test.ts index 3778c09b..8f4ef037 100644 --- a/src/split-map/split-map.fork.test.ts +++ b/src/split-map/split-map.fork.test.ts @@ -1,19 +1,31 @@ import 'regenerator-runtime/runtime'; -import { createDomain, fork, serialize, allSettled } from 'effector'; +import { + createDomain, + fork, + allSettled, + createEvent, + createStore, + createApi, +} from 'effector'; import { splitMap } from './index'; test('works in forked scope', async () => { - const app = createDomain(); - const source = app.createEvent<{ first?: number; another?: boolean }>(); + const $target = createStore(0); + + const source = createEvent<{ first?: number; another?: boolean }>(); const out = splitMap({ source, cases: { first: (payload) => payload.first, }, + targets: { + first: $target, + __: createApi($target, { another: (state) => -state }).another, + }, }); - const $data = app.createStore(0); + const $data = createStore(0); $data .on(out.first, (state, payload) => state + payload) @@ -25,24 +37,20 @@ test('works in forked scope', async () => { scope, params: { first: 15 }, }); - expect(serialize(scope)).toMatchInlineSnapshot(` - { - "xwy4bm": 15, - } - `); + expect(scope.getState($data)).toBe(15); + expect(scope.getState($target)).toBe(15); await allSettled(source, { scope, params: { another: true }, }); - expect(serialize(scope)).toMatchInlineSnapshot(` - { - "xwy4bm": -15, - } - `); + expect(scope.getState($data)).toBe(-15); + expect(scope.getState($target)).toBe(-15); }); test('do not affect another fork', async () => { + const $target = createStore(0); + const app = createDomain(); const source = app.createEvent<{ first?: number; another?: boolean }>(); const out = splitMap({ @@ -50,6 +58,9 @@ test('do not affect another fork', async () => { cases: { first: (payload) => payload.first, }, + targets: { + first: $target, + }, }); const $data = app.createStore(0); @@ -65,24 +76,20 @@ test('do not affect another fork', async () => { scope: scopeA, params: { first: 200 }, }); - expect(serialize(scopeA)).toMatchInlineSnapshot(` - { - "l4dkuf": 200, - } - `); + expect(scopeA.getState($data)).toBe(200); + expect(scopeA.getState($target)).toBe(200); await allSettled(source, { scope: scopeB, params: { first: -5 }, }); - expect(serialize(scopeB)).toMatchInlineSnapshot(` - { - "l4dkuf": -5, - } - `); + expect(scopeB.getState($data)).toBe(-5); + expect(scopeB.getState($target)).toBe(-5); }); test('do not affect original store value', async () => { + const $target = createStore(0); + const app = createDomain(); const source = app.createEvent<{ first?: number; another?: boolean }>(); const out = splitMap({ @@ -90,6 +97,9 @@ test('do not affect original store value', async () => { cases: { first: (payload) => payload.first, }, + targets: { + first: $target, + }, }); const $data = app.createStore(0); @@ -104,10 +114,10 @@ test('do not affect original store value', async () => { scope, params: { first: 15 }, }); - expect(serialize(scope)).toMatchInlineSnapshot(` - { - "8sunrf": 15, - } - `); + + expect(scope.getState($data)).toBe(15); expect($data.getState()).toBe($data.defaultState); + + expect(scope.getState($target)).toBe(15); + expect($target.getState()).toBe($target.defaultState); }); diff --git a/src/split-map/split-map.test.ts b/src/split-map/split-map.test.ts index 83019434..a19cb08d 100644 --- a/src/split-map/split-map.test.ts +++ b/src/split-map/split-map.test.ts @@ -1,4 +1,4 @@ -import { createEvent, createStore, is } from 'effector'; +import { createEffect, createEvent, createStore, is } from 'effector'; import { argumentHistory } from '../../test-library'; import { splitMap } from './index'; @@ -202,3 +202,90 @@ test('from readme', () => { watchUpdate.mockClear(); watchDefault.mockClear(); }); + +describe('with targets', () => { + it('should trigger units in targets', () => { + const source = createEvent<{ name?: string; age?: number }>(); + + const ageTarget = createEvent(); + const $nameTarget1 = createStore(''); + const nameTarget2 = createEvent(); + const defaultCaseTarget = createEffect(); + + const { nameStringed, ageNumbered, __ } = splitMap({ + source, + cases: { + ageNumbered: ({ age }) => age, + nameStringed: ({ name }) => name, + }, + targets: { + ageNumbered: ageTarget, + nameStringed: [$nameTarget1, nameTarget2], + __: defaultCaseTarget, + }, + }); + + const fnTargetAge = jest.fn(); + const fnTargetName1 = jest.fn(); + const fnTargetName2 = jest.fn(); + const fnTargetDefault = jest.fn(); + + const fnAgeNumbered = jest.fn(); + const fnNameStringed = jest.fn(); + const fnDefaultCase = jest.fn(); + + ageTarget.watch(fnTargetAge); + $nameTarget1.updates.watch(fnTargetName1); + nameTarget2.watch(fnTargetName2); + defaultCaseTarget.watch(fnTargetDefault); + + ageNumbered.watch(fnAgeNumbered); + nameStringed.watch(fnNameStringed); + __.watch(fnDefaultCase); + + source({ age: 100 }); + + expect(fnAgeNumbered).toHaveBeenNthCalledWith(1, 100); + expect(fnTargetAge).toHaveBeenNthCalledWith(1, 100); + + source({ name: 'John' }); + + expect(fnNameStringed).toHaveBeenNthCalledWith(1, 'John'); + expect(fnTargetName1).toHaveBeenNthCalledWith(1, 'John'); + + source({}); + + expect(fnDefaultCase).toBeCalledTimes(1); + expect(fnTargetDefault).toBeCalledTimes(1); + }); + + it('should trigger all units in default case', () => { + const source = createEvent(); + + const target1 = createEvent(); + const $target2 = createStore(''); + const target3 = createEffect(); + + splitMap({ + source, + cases: {}, + targets: { + __: [target1, $target2, target3], + }, + }); + + const fnTarget1 = jest.fn(); + const fnTarget2 = jest.fn(); + const fnTarget3 = jest.fn(); + + target1.watch(fnTarget1); + $target2.updates.watch(fnTarget2); + target3.watch(fnTarget3); + + source('Demo'); + + expect(fnTarget1).toHaveBeenNthCalledWith(1, 'Demo'); + expect(fnTarget2).toHaveBeenNthCalledWith(1, 'Demo'); + expect(fnTarget3).toHaveBeenNthCalledWith(1, 'Demo'); + }); +}); diff --git a/test-typings/split-map.ts b/test-typings/split-map.ts index 1894292a..b16bb76b 100644 --- a/test-typings/split-map.ts +++ b/test-typings/split-map.ts @@ -68,6 +68,50 @@ import { splitMap } from '../dist/split-map'; ); } +// Allow any unit as source (with targets) +{ + const event = createEvent(); + const $store = createStore(0); + const effect = createEffect(); + + expectType<{ demo: Event; __: Event }>( + splitMap({ + source: event, + cases: { + demo: () => true, + }, + targets: { + demo: createEvent(), + __: createStore(0), + }, + }), + ); + expectType<{ demo: Event; __: Event }>( + splitMap({ + source: $store, + cases: { + demo: () => true, + }, + targets: { + demo: createEffect(), + __: createEvent(), + }, + }), + ); + expectType<{ demo: Event; __: Event }>( + splitMap({ + source: effect, + cases: { + demo: () => true, + }, + targets: { + demo: createStore(true), + __: createEffect(), + }, + }), + ); +} + // Has default case { expectType<{ __: Event }>( @@ -76,6 +120,26 @@ import { splitMap } from '../dist/split-map'; expectType<{ __: Event<{ demo: number }> }>( splitMap({ source: createEvent<{ demo: number }>(), cases: {} }), ); + + // with targets + expectType<{ __: Event }>( + splitMap({ + source: createEvent(), + cases: {}, + targets: { + __: createEvent(), + }, + }), + ); + expectType<{ __: Event<{ demo: number }> }>( + splitMap({ + source: createEvent<{ demo: number }>(), + cases: {}, + targets: { + __: createEvent<{ demo: number }>(), + }, + }), + ); } // Omit undefined from object type @@ -93,4 +157,147 @@ import { splitMap } from '../dist/split-map'; }, }), ); + + expectType<{ example: Event; __: Event }>( + splitMap({ + source, + cases: { + example: (object) => object.key, + }, + targets: { + example: createEvent(), + __: createEvent(), + }, + }), + ); +} + +// Allow void units in targets +{ + splitMap({ + source: createEvent<{ name?: string; age?: number }>(), + cases: { + ageNumbered: ({ age }) => age, + nameStringed: ({ name }) => name, + doSome: () => true, + doAnother: () => null, + }, + targets: { + ageNumbered: createEvent(), + nameStringed: [createStore(''), createEffect()], + doSome: [createEvent(), createEvent()], + doAnother: createEvent(), + __: createEvent(), + }, + }); +} + +// Allow array of units in targets default case +{ + splitMap({ + source: createEvent(), + cases: {}, + targets: { + __: [ + createEvent(), + createStore(''), + createEffect(), + ], + }, + }); + + // Allow void units in targets default case + splitMap({ + source: createEvent(), + cases: {}, + targets: { + __: [createStore(''), createEvent(), createEffect()], + }, + }); +} + +// Expect matching targets types with cases +{ + splitMap({ + source: createEvent<{ name?: string; age?: number }>(), + cases: { + ageNumbered: ({ age }) => age, + nameStringed: ({ name }) => name, + doSome: () => true, + doAnother: () => null, + foo: () => '', + }, + targets: { + // @ts-expect-error + ageNumbered: createEvent(), + nameStringed: [ + // @ts-expect-error + createStore(''), + // @ts-expect-error + createEffect(), + ], + doSome: [ + // @ts-expect-error + createEvent(), + createEvent(), + ], + doAnother: [ + // @ts-expect-error + createEvent(), + // @ts-expect-error + createEffect(), + ], + foo: [ + // @ts-expect-error + createEvent(), + // @ts-expect-error + createEvent(), + ], + }, + }); + + splitMap({ + source: createEvent(), + cases: {}, + targets: { + // @ts-expect-error + __: createEvent(), + }, + }); + + splitMap({ + source: createEvent>(), + cases: {}, + targets: { + __: [ + // @ts-expect-error + createStore(''), + // @ts-expect-error + createEvent(), + // @ts-expect-error + createEffect(), + + createEvent(), + ], + }, + }); +} + +// case payloads should extend target units +{ + splitMap({ + source: createEvent<{ first?: string; second?: number }>(), + cases: { + first: ({ first }) => first, // will be string + second: ({ second }) => second, // will be number + }, + targets: { + // string should extend string | null + first: createEvent(), + + // number should extend number | null + // TODO: but not string, should expect error + second: [createEvent(), createEvent()], + }, + }); } From 1b30ae3965d72f17ca4a8f98cfb7586d6c0b0cd9 Mon Sep 17 00:00:00 2001 From: earthspacon Date: Tue, 20 Aug 2024 13:46:28 +0500 Subject: [PATCH 2/4] feat(splitMap): updated read.me --- src/split-map/readme.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/split-map/readme.md b/src/split-map/readme.md index e9f7c949..d8885ef6 100644 --- a/src/split-map/readme.md +++ b/src/split-map/readme.md @@ -131,7 +131,7 @@ splitMap({ source, cases, targets }); ### Arguments -1. `source` ( | | ) — Source unit, data from this unit passed to each function in `cases` object and `__` event in `shape` as is +1. `source` ([_`Event`_] | [_`Store`_] | [_`Effect`_]) — Source unit, data from this unit passed to each function in `cases` object and `__` event in `shape` as is 2. `cases` (`{ [key: string]: (payload: T) => any | void }`) — Object of functions. Function receives one argument is a payload from `source`, should return any value or `undefined` 3. `targets` (`{ [key: string]?: Unit | Unit[]; __?: Unit | Unit[] }`) — Object of units to trigger on corresponding event from `cases` object @@ -139,6 +139,10 @@ splitMap({ source, cases, targets }); - `shape` (`{ [key: string]: Event; __: Event }`) — Object of events, with the same structure as `cases`, but with the _default_ event `__`, that triggered when each other function returns `undefined` +[_`event`_]: https://effector.dev/docs/api/effector/event +[_`effect`_]: https://effector.dev/docs/api/effector/effect +[_`store`_]: https://effector.dev/docs/api/effector/store + ### Examples #### Split WebSocket events to effector events with targets @@ -164,7 +168,7 @@ splitMap({ source: websocketEventReceived, cases: { init: (event) => { - if (event.type === 'init') return { init: true, key: event.key }; + if (event.type === 'init') return { init: true, dataId: event.key }; }, increment: (payload) => { if (payload.type === 'increment') return payload.count; @@ -174,8 +178,8 @@ splitMap({ }, }, targets: { - // Event<{ init?: boolean; key?: string }> - init: spread({ init: $isInitialized, key: getInitialDataFx }), + // EventCallable<{ init?: boolean; dataId?: string }> + init: spread({ init: $isInitialized, dataId: getInitialDataFx }), increment: $count, reset: [$count.reinit, $isInitialized.reinit], }, @@ -198,7 +202,7 @@ const { init } = splitMap({ source: websocketEventReceived, cases: { init: (event) => { - if (event.type === 'init') return { init: true, key: event.key }; + if (event.type === 'init') return { init: true, dataId: event.key }; }, increment: (payload) => { if (payload.type === 'increment') return payload.count; @@ -208,8 +212,8 @@ const { init } = splitMap({ }, }, targets: { - // Event<{ init?: boolean; key?: string }> - init: spread({ init: $isInitialized, key: getInitialDataFx }), + // EventCallable<{ init?: boolean; dataId?: string }> + init: spread({ init: $isInitialized, dataId: getInitialDataFx }), increment: $count, reset: [$count.reinit, $isInitialized.reinit], }, From e50155002e20376964c26077099db206e94c11d9 Mon Sep 17 00:00:00 2001 From: earthspacon Date: Fri, 27 Sep 2024 11:42:50 +0500 Subject: [PATCH 3/4] ref(splitMap): rewrote tests to not modify existing ones --- src/split-map/split-map.fork.test.ts | 172 ++++++++++++++++++++++----- 1 file changed, 142 insertions(+), 30 deletions(-) diff --git a/src/split-map/split-map.fork.test.ts b/src/split-map/split-map.fork.test.ts index 8f4ef037..7db526ae 100644 --- a/src/split-map/split-map.fork.test.ts +++ b/src/split-map/split-map.fork.test.ts @@ -6,26 +6,22 @@ import { createEvent, createStore, createApi, + serialize, } from 'effector'; import { splitMap } from './index'; test('works in forked scope', async () => { - const $target = createStore(0); - - const source = createEvent<{ first?: number; another?: boolean }>(); + const app = createDomain(); + const source = app.createEvent<{ first?: number; another?: boolean }>(); const out = splitMap({ source, cases: { first: (payload) => payload.first, }, - targets: { - first: $target, - __: createApi($target, { another: (state) => -state }).another, - }, }); - const $data = createStore(0); + const $data = app.createStore(0); $data .on(out.first, (state, payload) => state + payload) @@ -37,20 +33,24 @@ test('works in forked scope', async () => { scope, params: { first: 15 }, }); - expect(scope.getState($data)).toBe(15); - expect(scope.getState($target)).toBe(15); + expect(serialize(scope)).toMatchInlineSnapshot(` + { + "xwy4bm": 15, + } + `); await allSettled(source, { scope, params: { another: true }, }); - expect(scope.getState($data)).toBe(-15); - expect(scope.getState($target)).toBe(-15); + expect(serialize(scope)).toMatchInlineSnapshot(` + { + "xwy4bm": -15, + } + `); }); test('do not affect another fork', async () => { - const $target = createStore(0); - const app = createDomain(); const source = app.createEvent<{ first?: number; another?: boolean }>(); const out = splitMap({ @@ -58,9 +58,6 @@ test('do not affect another fork', async () => { cases: { first: (payload) => payload.first, }, - targets: { - first: $target, - }, }); const $data = app.createStore(0); @@ -76,20 +73,24 @@ test('do not affect another fork', async () => { scope: scopeA, params: { first: 200 }, }); - expect(scopeA.getState($data)).toBe(200); - expect(scopeA.getState($target)).toBe(200); + expect(serialize(scopeA)).toMatchInlineSnapshot(` + { + "l4dkuf": 200, + } + `); await allSettled(source, { scope: scopeB, params: { first: -5 }, }); - expect(scopeB.getState($data)).toBe(-5); - expect(scopeB.getState($target)).toBe(-5); + expect(serialize(scopeB)).toMatchInlineSnapshot(` + { + "l4dkuf": -5, + } + `); }); test('do not affect original store value', async () => { - const $target = createStore(0); - const app = createDomain(); const source = app.createEvent<{ first?: number; another?: boolean }>(); const out = splitMap({ @@ -97,9 +98,6 @@ test('do not affect original store value', async () => { cases: { first: (payload) => payload.first, }, - targets: { - first: $target, - }, }); const $data = app.createStore(0); @@ -114,10 +112,124 @@ test('do not affect original store value', async () => { scope, params: { first: 15 }, }); - - expect(scope.getState($data)).toBe(15); + expect(serialize(scope)).toMatchInlineSnapshot(` + { + "8sunrf": 15, + } + `); expect($data.getState()).toBe($data.defaultState); +}); - expect(scope.getState($target)).toBe(15); - expect($target.getState()).toBe($target.defaultState); +describe('splitMap with targets', () => { + test('works in forked scope', async () => { + const $target = createStore(0); + + const source = createEvent<{ first?: number; another?: boolean }>(); + const out = splitMap({ + source, + cases: { + first: (payload) => payload.first, + }, + targets: { + first: $target, + __: createApi($target, { another: (state) => -state }).another, + }, + }); + + const $data = createStore(0); + + $data + .on(out.first, (state, payload) => state + payload) + .on(out.__, (state, payload) => (payload ? -state : 0)); + + const scope = fork(); + + await allSettled(source, { + scope, + params: { first: 15 }, + }); + expect(scope.getState($data)).toBe(15); + expect(scope.getState($target)).toBe(15); + + await allSettled(source, { + scope, + params: { another: true }, + }); + expect(scope.getState($data)).toBe(-15); + expect(scope.getState($target)).toBe(-15); + }); + + test('do not affect another fork', async () => { + const $target = createStore(0); + + const app = createDomain(); + const source = app.createEvent<{ first?: number; another?: boolean }>(); + const out = splitMap({ + source, + cases: { + first: (payload) => payload.first, + }, + targets: { + first: $target, + }, + }); + + const $data = app.createStore(0); + + $data + .on(out.first, (state, payload) => state + payload) + .on(out.__, (state, payload) => (payload ? -state : 0)); + + const scopeA = fork(); + const scopeB = fork(); + + await allSettled(source, { + scope: scopeA, + params: { first: 200 }, + }); + expect(scopeA.getState($data)).toBe(200); + expect(scopeA.getState($target)).toBe(200); + + await allSettled(source, { + scope: scopeB, + params: { first: -5 }, + }); + expect(scopeB.getState($data)).toBe(-5); + expect(scopeB.getState($target)).toBe(-5); + }); + + test('do not affect original store value', async () => { + const $target = createStore(0); + + const app = createDomain(); + const source = app.createEvent<{ first?: number; another?: boolean }>(); + const out = splitMap({ + source, + cases: { + first: (payload) => payload.first, + }, + targets: { + first: $target, + }, + }); + + const $data = app.createStore(0); + + $data + .on(out.first, (state, payload) => state + payload) + .on(out.__, (state, payload) => (payload ? -state : 0)); + + const scope = fork(); + + await allSettled(source, { + scope, + params: { first: 15 }, + }); + + expect(scope.getState($data)).toBe(15); + expect($data.getState()).toBe($data.defaultState); + + expect(scope.getState($target)).toBe(15); + expect($target.getState()).toBe($target.defaultState); + }); }); From 8e76e5c867649ca535efe5505f2871463c111d83 Mon Sep 17 00:00:00 2001 From: earthspacon Date: Fri, 27 Sep 2024 13:05:43 +0500 Subject: [PATCH 4/4] feat(splitMap): added jsdoc --- src/split-map/index.ts | 70 ++++++++++++++++++++++++++++++++++++++++- src/split-map/readme.md | 4 ++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/split-map/index.ts b/src/split-map/index.ts index ff0a2868..7fa3266f 100644 --- a/src/split-map/index.ts +++ b/src/split-map/index.ts @@ -9,6 +9,74 @@ const hasPropBase = {}.hasOwnProperty; const hasOwnProp = (object: O, key: string) => hasPropBase.call(object, key); +/** + * Split `source` unit into multiple events based on the provided `cases`. + * + * @param source - Source unit, data from this unit is passed to each function in cases object and `__` event in shape as is + * @param cases - Object of functions. Function receives one argument is a payload from `source`, should return any value or `undefined`. + * If `undefined` is returned from the case function, update will be skipped (the event will not be triggered). + * + * @param targets (optional) - Object of units to trigger on corresponding event from cases object + * @returns Object of events, with the same structure as `cases`, but with the default event `__`, that will be triggered when each other function returns `undefined` + * + * @example + * ```ts + * const dataFetched = createEvent() + * + * const dataReceived = splitMap({ + * source: dataFetched, + * cases: { + * isString: (payload) => { + * if (typeof payload === 'string') return payload + * }, + * isNumber: (payload) => { + * if (typeof payload === 'number') return payload + * }, + * } + * }) + + * dataReceived.isString // Event + * dataReceived.isNumber // Event + * dataReceived.__ // Event + * ``` + * + * @example + * ```ts + * const dataFetched = createEvent() + * const stringReceived = createEvent() + * const numberReceived = createEvent() + * const unknownReceived = createEvent() + * const notifyError = createEvent() + * + * const dataReceived = splitMap({ + * source: dataFetched, + * cases: { + * isString: (payload) => { + * if (typeof payload === 'string') return payload + * }, + * isNumber: (payload) => { + * if (typeof payload === 'number') return payload + * }, + * }, + * targets: { + * isString: stringReceived, + * isNumber: numberReceived, + * __: [unknownReceived, notifyError], + * }, + * }) + * + * dataFetched('string') + * // => stringReceived('string') + * + * dataFetched(42) + * // => numberReceived(42) + * + * dataFetched(null) + * // => unknownReceived(null) + * // => notifyError() + * ``` + */ + export function splitMap< S, Cases extends Record any | undefined>, @@ -64,7 +132,7 @@ export function splitMap< // eslint-disable-next-line no-underscore-dangle result.__ = current; - if (targets && '__' in targets) { + if (targets && hasOwnProp(targets, '__')) { const defaultCaseTarget = targets.__; sample({ diff --git a/src/split-map/readme.md b/src/split-map/readme.md index abd1de12..689d1451 100644 --- a/src/split-map/readme.md +++ b/src/split-map/readme.md @@ -142,7 +142,7 @@ splitMap({ source, cases, targets }); ### Returns -- `shape` (`{ [key: string]: Event; __: Event }`) — Object of events, with the same structure as `cases`, but with the _default_ event `__`, that triggered when each other function returns `undefined` +- `shape` (`{ [key: string]: Event; __: Event }`) — Object of events, with the same structure as `cases`, but with the _default_ event `__`, that will be triggered when each other function returns `undefined` [_`event`_]: https://effector.dev/docs/api/effector/event [_`effect`_]: https://effector.dev/docs/api/effector/effect @@ -163,6 +163,7 @@ type WSEvent = | { type: 'reset' }; export const websocketEventReceived = createEvent(); +const notifyError = createEvent(); const $isInitialized = createStore(false); const $count = createStore(null); @@ -187,6 +188,7 @@ splitMap({ init: spread({ init: $isInitialized, dataId: getInitialDataFx }), increment: $count, reset: [$count.reinit, $isInitialized.reinit], + __: notifyError, }, });