Skip to content

Commit 9bcf90b

Browse files
committed
feat(decorator): introduce ImmerReducer
fix #20
1 parent 7f0d6c4 commit 9bcf90b

File tree

10 files changed

+170
-9
lines changed

10 files changed

+170
-9
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"eslint-config-prettier": "^4.1.0",
7676
"eslint-plugin-react": "^7.12.4",
7777
"husky": "^2.1.0",
78+
"immer": "^3.1.2",
7879
"jest": "^24.5.0",
7980
"lint-staged": "^8.1.5",
8081
"lodash": "^4.17.11",
@@ -94,6 +95,7 @@
9495
},
9596
"peerDependencies": {
9697
"@asuka/di": "^0.1.3",
98+
"immer": "^3.1.2",
9799
"lodash": "^4.17.11",
98100
"react": "^16.8.6",
99101
"reflect-metadata": "^0.1.13",

src/core/decorators/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Observable } from 'rxjs'
2+
import { Draft } from 'immer'
23

3-
import { defineActionSymbols, effectSymbols, reducerSymbols } from '../symbols'
4+
import { defineActionSymbols, effectSymbols, reducerSymbols, immerReducerSymbols } from '../symbols'
45
import { EffectAction } from '../types'
56
import { createActionDecorator } from './action-related'
67

@@ -10,6 +11,10 @@ interface DecoratorReturnType<V> {
1011
(target: any, propertyKey: string, descriptor: { value?: V; get?(): V }): void
1112
}
1213

14+
export const ImmerReducer: <S = any>() => DecoratorReturnType<
15+
(state: Draft<S>, params?: any) => void
16+
> = createActionDecorator(immerReducerSymbols)
17+
1318
export const Reducer: <S = any>() => DecoratorReturnType<
1419
(state: S, params?: any) => S
1520
> = createActionDecorator(reducerSymbols)

src/core/ikari.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { merge, Observable, Subject, Subscription, NEVER } from 'rxjs'
22
import { map, catchError } from 'rxjs/operators'
33
import { mapValues } from 'lodash'
4+
import produce from 'immer'
45

56
import {
67
EffectAction,
78
ReducerAction,
89
OriginalEffectActions,
910
OriginalReducerActions,
11+
OriginalImmerReducerActions,
1012
OriginalDefineActions,
1113
TriggerActions,
1214
EffectActionFactories,
@@ -21,6 +23,7 @@ interface Config<State> {
2123
defaultState: State
2224
effects: OriginalEffectActions<State>
2325
reducers: OriginalReducerActions<State>
26+
immerReducers: OriginalImmerReducerActions<State>
2427
defineActions: OriginalDefineActions
2528
effectActionFactories: EffectActionFactories
2629
}
@@ -45,7 +48,7 @@ export function combineWithIkari<S>(ayanami: Ayanami<S>): Ikari<S> {
4548
if (ikari) {
4649
return ikari
4750
} else {
48-
const { effects, reducers, defineActions } = getOriginalFunctions(ayanami)
51+
const { effects, reducers, immerReducers, defineActions } = getOriginalFunctions(ayanami)
4952

5053
Object.assign(ayanami, mapValues(defineActions, ({ observable }) => observable))
5154

@@ -54,6 +57,7 @@ export function combineWithIkari<S>(ayanami: Ayanami<S>): Ikari<S> {
5457
defaultState: ayanami.defaultState,
5558
effects,
5659
reducers,
60+
immerReducers,
5761
defineActions,
5862
effectActionFactories: getEffectActionFactories(ayanami),
5963
})
@@ -108,9 +112,15 @@ export class Ikari<State> {
108112
this.state.getState,
109113
)
110114

115+
const [immerReducerActions$, immerReducerActions] = setupImmerReducerActions(
116+
this.config.immerReducers,
117+
this.state.getState,
118+
)
119+
111120
this.triggerActions = {
112121
...effectActions,
113122
...reducerActions,
123+
...immerReducerActions,
114124
...mapValues(this.config.defineActions, ({ next }) => next),
115125
}
116126

@@ -127,6 +137,13 @@ export class Ikari<State> {
127137
this.handleAction(action)
128138
}),
129139
)
140+
141+
this.subscription.add(
142+
immerReducerActions$.subscribe((action) => {
143+
this.log(action)
144+
this.handleAction(action)
145+
}),
146+
)
130147
}
131148

132149
destroy() {
@@ -218,3 +235,31 @@ function setupReducerActions<State>(
218235

219236
return [merge(...reducers), actions]
220237
}
238+
239+
function setupImmerReducerActions<State>(
240+
immerReducerActions: OriginalImmerReducerActions<State>,
241+
getState: () => State,
242+
): [Observable<Action<State>>, TriggerActions] {
243+
const actions: TriggerActions = {}
244+
const immerReducers: Observable<Action<State>>[] = []
245+
246+
Object.keys(immerReducerActions).forEach((actionName) => {
247+
const immerReducer$ = new Subject<Action<State>>()
248+
immerReducers.push(immerReducer$)
249+
250+
const immerReducer = immerReducerActions[actionName]
251+
252+
actions[actionName] = (params: any) => {
253+
const nextState = produce(getState(), (draft) => {
254+
immerReducer(draft, params)
255+
})
256+
257+
immerReducer$.next({
258+
reducerAction: { params, actionName, nextState },
259+
originalActionName: actionName,
260+
})
261+
}
262+
})
263+
264+
return [merge(...immerReducers), actions]
265+
}

src/core/symbols.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,21 @@ export const reducerSymbols: ActionSymbols = {
1313
actions: Symbol('actions:reducer'),
1414
}
1515

16+
export const immerReducerSymbols: ActionSymbols = {
17+
decorator: Symbol('decorator:immer-reducer'),
18+
actions: Symbol('actions:immer-reducer'),
19+
}
20+
1621
export const defineActionSymbols: ActionSymbols = {
1722
decorator: Symbol('decorator:defineAction'),
1823
actions: Symbol('actions:defineAction'),
1924
}
2025

21-
export const allActionSymbols = [effectSymbols, reducerSymbols, defineActionSymbols]
26+
export const allActionSymbols = [
27+
effectSymbols,
28+
reducerSymbols,
29+
immerReducerSymbols,
30+
defineActionSymbols,
31+
]
2232

2333
export const ikariSymbol = Symbol('ikari')

src/core/types.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Observable } from 'rxjs'
2+
import { Draft } from 'immer'
23

34
import { Ayanami } from './ayanami'
45

@@ -77,11 +78,19 @@ type UnpackReducerPayload<Func, State> = Func extends () => State
7778
? UnpackReducerFunctionArguments<Func>
7879
: never
7980

81+
type UnpackImmerReducerPayload<Func, State> = Func extends (state: Draft<State>) => void
82+
? UnpackReducerFunctionArguments<Func>
83+
: Func extends (state: Draft<State>, payload: any) => void
84+
? UnpackReducerFunctionArguments<Func>
85+
: never
86+
8087
type UnpackDefineActionPayload<OB> = OB extends Observable<infer P> ? ArgumentsType<[P]> : never
8188

8289
type UnpackPayload<F, S> = UnpackEffectPayload<F, S> extends never
8390
? UnpackReducerPayload<F, S> extends never
84-
? UnpackDefineActionPayload<F>
91+
? UnpackImmerReducerPayload<F, S> extends never
92+
? UnpackDefineActionPayload<F>
93+
: UnpackImmerReducerPayload<F, S>
8594
: UnpackReducerPayload<F, S>
8695
: UnpackEffectPayload<F, S>
8796

@@ -115,6 +124,10 @@ export type OriginalReducerActions<State> = ObjectOf<
115124
(state: State, payload: any) => Readonly<State>
116125
>
117126

127+
export type OriginalImmerReducerActions<State> = ObjectOf<
128+
(state: Draft<State>, payload: any) => void
129+
>
130+
118131
export type OriginalDefineActions = ObjectOf<{
119132
next(params: any): void
120133
observable: Observable<any>

src/core/utils/get-original-functions.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import { Subject } from 'rxjs'
22
import { pick, mapValues } from 'lodash'
33

4-
import { OriginalDefineActions, OriginalEffectActions, OriginalReducerActions } from '../types'
4+
import {
5+
OriginalDefineActions,
6+
OriginalEffectActions,
7+
OriginalReducerActions,
8+
OriginalImmerReducerActions,
9+
} from '../types'
510
import { Ayanami } from '../ayanami'
611
import { getActionNames } from '../decorators'
7-
import { effectSymbols, reducerSymbols, defineActionSymbols } from '../symbols'
12+
import { effectSymbols, reducerSymbols, immerReducerSymbols, defineActionSymbols } from '../symbols'
813

914
const getOriginalFunctionNames = (ayanami: Ayanami<any>) => ({
1015
effects: getActionNames(effectSymbols, ayanami.constructor),
1116
reducers: getActionNames(reducerSymbols, ayanami.constructor),
1217
defineActions: getActionNames(defineActionSymbols, ayanami.constructor),
18+
immerReducers: getActionNames(immerReducerSymbols, ayanami.constructor),
1319
})
1420

1521
const transformDefineActions = (actionNames: string[]): OriginalDefineActions => {
@@ -28,7 +34,7 @@ const transformDefineActions = (actionNames: string[]): OriginalDefineActions =>
2834
}
2935

3036
export const getOriginalFunctions = (ayanami: Ayanami<any>) => {
31-
const { effects, reducers, defineActions } = getOriginalFunctionNames(ayanami)
37+
const { effects, reducers, immerReducers, defineActions } = getOriginalFunctionNames(ayanami)
3238

3339
return {
3440
effects: mapValues(pick(ayanami, effects), (func: Function) =>
@@ -37,6 +43,9 @@ export const getOriginalFunctions = (ayanami: Ayanami<any>) => {
3743
reducers: mapValues(pick(ayanami, reducers), (func: Function) =>
3844
func.bind(ayanami),
3945
) as OriginalReducerActions<any>,
46+
immerReducers: mapValues(pick(ayanami, immerReducers), (func: Function) =>
47+
func.bind(ayanami),
48+
) as OriginalImmerReducerActions<any>,
4049
defineActions: transformDefineActions(defineActions),
4150
}
4251
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'reflect-metadata'
22

33
export {
44
Ayanami,
5+
ImmerReducer,
56
Reducer,
67
Effect,
78
DefineAction,

test/specs/ikari.spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Subject, NEVER } from 'rxjs'
2+
import { Draft } from 'immer'
23

34
import '../../src'
45
import { Ikari, BasicState } from '../../src/core'
@@ -23,6 +24,11 @@ const createIkariConfig = () => ({
2324
reducers: {
2425
setCount: (state: State, count: number): State => ({ ...state, count }),
2526
},
27+
immerReducers: {
28+
immerSetCount: (state: Draft<State>, count: number) => {
29+
state.count = count
30+
},
31+
},
2632
defineActions: { hmm: getDefineAction() },
2733
effectActionFactories: {},
2834
})
@@ -49,17 +55,23 @@ describe('Ikari spec:', () => {
4955
expect(ikari.state.getState()).toEqual({ count: 0 })
5056
})
5157

52-
it('triggerActions is combination of effects, reducers and defineActions', () => {
53-
expect(Object.keys(ikari.triggerActions).length).toBe(3)
58+
it('triggerActions is combination of effects, reducers, immerReducers and defineActions', () => {
59+
expect(Object.keys(ikari.triggerActions).length).toBe(4)
5460
expect(typeof ikari.triggerActions.never).toBe('function')
5561
expect(typeof ikari.triggerActions.setCount).toBe('function')
62+
expect(typeof ikari.triggerActions.immerSetCount).toBe('function')
5663
expect(typeof ikari.triggerActions.hmm).toBe('function')
5764
})
5865

5966
it('reducers can change state', () => {
6067
ikari.triggerActions.setCount(1)
6168
expect(ikari.state.getState()).toEqual({ count: 1 })
6269
})
70+
71+
it('ImmerReducers can change state', () => {
72+
ikari.triggerActions.immerSetCount(2)
73+
expect(ikari.state.getState()).toEqual({ count: 2 })
74+
})
6375
})
6476

6577
describe('after destroy', () => {

test/specs/immer-reducer.spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Injectable, Test } from '@asuka/di'
2+
import { Draft } from 'immer'
3+
4+
import { Ayanami, ImmerReducer, getAllActionsForTest, ActionMethodOfAyanami } from '../../src'
5+
6+
interface TipsState {
7+
tips: string
8+
}
9+
10+
@Injectable()
11+
class Tips extends Ayanami<TipsState> {
12+
defaultState = {
13+
tips: '',
14+
}
15+
16+
@ImmerReducer()
17+
removeTips(state: Draft<TipsState>) {
18+
state.tips = ''
19+
}
20+
21+
@ImmerReducer()
22+
setTips(state: Draft<TipsState>, tips: string) {
23+
state.tips = tips
24+
}
25+
26+
@ImmerReducer()
27+
addTips(state: Draft<TipsState>, tips: string) {
28+
state.tips = `${state.tips} ${tips}`
29+
}
30+
}
31+
32+
describe('ImmerReducer spec:', () => {
33+
let tips: Tips
34+
let actions: ActionMethodOfAyanami<Tips, TipsState>
35+
36+
beforeEach(() => {
37+
const testModule = Test.createTestingModule().compile()
38+
39+
tips = testModule.getInstance(Tips)
40+
actions = getAllActionsForTest(tips)
41+
})
42+
43+
it('with payload', () => {
44+
actions.setTips('one')
45+
expect(tips.getState()).toEqual({ tips: 'one' })
46+
})
47+
48+
it('with payload and state', () => {
49+
actions.setTips('two')
50+
actions.addTips('three')
51+
expect(tips.getState()).toEqual({ tips: 'two three' })
52+
})
53+
54+
it('without payload and state', () => {
55+
actions.setTips('one')
56+
actions.removeTips()
57+
expect(tips.getState()).toEqual({ tips: '' })
58+
})
59+
})

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3678,6 +3678,11 @@ ignore@^4.0.2:
36783678
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
36793679
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
36803680

3681+
immer@^3.1.2:
3682+
version "3.1.2"
3683+
resolved "https://registry.npmjs.org/immer/-/immer-3.1.2.tgz#1f24f2e25433f5ff168a1ff6c8c6366d2106e61a"
3684+
integrity sha512-pi8JuvJ9c+98DlpDms/o0YLLg5khP8Qzjd8CtIGc4qVm2eoLdQGivJL266nWccofKT/oBSxnhAh/o+nmOXtLXw==
3685+
36813686
import-fresh@^2.0.0:
36823687
version "2.0.0"
36833688
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"

0 commit comments

Comments
 (0)