Skip to content

Commit 740dec2

Browse files
feat(overmind): dynamic derived
1 parent 795fd91 commit 740dec2

File tree

8 files changed

+161
-46
lines changed

8 files changed

+161
-46
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@
3030
"axios": "0.19.0",
3131
"color": "3.0.0",
3232
"color-hash": "1.0.3",
33-
"electron": "^8.0.0",
34-
"electron-json-storage": "^4.1.8",
35-
"electron-prompt": "^1.5.1",
33+
"electron": "8.0.0",
34+
"electron-json-storage": "4.1.8",
35+
"electron-prompt": "1.5.1",
3636
"emotion": "9.2.12",
3737
"express": "4.16.3",
3838
"graphql": "14.5.8",
@@ -111,7 +111,7 @@
111111
"ts-loader": "4.4.2",
112112
"tslib": "1.10.0",
113113
"tslint": "5.12.1",
114-
"typescript": "^3.7.2",
114+
"typescript": "^3.7.5",
115115
"typescript-eslint-parser": "^21.0.1",
116116
"url-loader": "1.0.1",
117117
"vscode": "1.1.36",

packages/node_modules/overmind/src/derived.test.ts

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { PROXY_TREE } from 'proxy-state-tree'
22

3-
import { IAction, IDerive, IState, Overmind } from './'
3+
import { EventType, IAction, IDerive, IState, Overmind, derived } from './'
44

55
type State = {
66
foo: string
@@ -31,6 +31,114 @@ describe('Derived', () => {
3131
expect(app.state.upperFoo).toEqual('BAR')
3232
})
3333

34+
test('should dynamically remove derived', async () => {
35+
let dirtyCount = 0
36+
const removeDerived: Action = ({ state }) => {
37+
state.upperFoo = null as any
38+
}
39+
const changeFoo: Action<number> = ({ state }, count) => {
40+
state.foo = 'bar' + count
41+
}
42+
type State = {
43+
foo: string
44+
upperFoo: Derive<State, string | null>
45+
}
46+
const state: State = {
47+
foo: 'bar',
48+
upperFoo: (state) => state.foo.toUpperCase()
49+
}
50+
51+
const config = {
52+
state,
53+
actions: {
54+
changeFoo,
55+
removeDerived
56+
},
57+
}
58+
type Config = {
59+
state: {
60+
foo: string
61+
upperFoo: string
62+
}
63+
actions: typeof config.actions
64+
}
65+
interface Action<Input = void> extends IAction<Config, Input> {}
66+
interface Derive<Parent extends IState, Value>
67+
extends IDerive<Config, Parent, Value> {}
68+
69+
const app = new Overmind(config)
70+
const trackStateTree = app.getTrackStateTree()
71+
const onTrack = () => {
72+
73+
}
74+
function render() {
75+
trackStateTree.track(onTrack)
76+
app.state.upperFoo
77+
}
78+
app.eventHub.on(EventType.DERIVED_DIRTY, () => {
79+
dirtyCount++
80+
})
81+
render()
82+
expect(app.state.upperFoo).toBe('BAR')
83+
app.actions.changeFoo(2)
84+
expect(app.state.upperFoo).toBe('BAR2')
85+
app.actions.removeDerived()
86+
app.actions.changeFoo(3)
87+
// The dirty event is async
88+
await new Promise(resolve => setTimeout(resolve, 0))
89+
expect(dirtyCount).toBe(1)
90+
})
91+
92+
test('should dynamically add derived', () => {
93+
const addDerived: Action = ({ state }) => {
94+
state.upperFoo = derived<Derive<typeof state, string>>((state) => state.foo.toUpperCase())
95+
}
96+
const changeFoo: Action = ({ state }) => {
97+
state.foo = 'bar2'
98+
}
99+
type State = {
100+
foo: string
101+
upperFoo: Derive<State, string> | null
102+
}
103+
const state: State = {
104+
foo: 'bar',
105+
upperFoo: null
106+
}
107+
108+
const config = {
109+
state,
110+
actions: {
111+
changeFoo,
112+
addDerived
113+
},
114+
}
115+
type Config = {
116+
state: {
117+
foo: string
118+
upperFoo: string
119+
}
120+
actions: typeof config.actions
121+
}
122+
interface Action<Input = void> extends IAction<Config, Input> {}
123+
interface Derive<Parent extends IState, Value>
124+
extends IDerive<Config, Parent, Value> {}
125+
126+
const app = new Overmind(config)
127+
const trackStateTree = app.getTrackStateTree()
128+
const onTrack = () => {
129+
130+
}
131+
function render() {
132+
trackStateTree.track(onTrack)
133+
app.state.upperFoo
134+
}
135+
render()
136+
app.actions.addDerived()
137+
expect(app.state.upperFoo).toBe('BAR')
138+
app.actions.changeFoo()
139+
expect(app.state.upperFoo).toBe('BAR2')
140+
})
141+
34142
test('should allow access to proxy state tree to continue tracking with derived function', () => {
35143
let renderCount = 0
36144
const changeFoo: Action = ({ state }) => {

packages/node_modules/overmind/src/derived.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { EventEmitter } from 'betsy'
22
import {
3-
ProxyStateTree,
4-
VALUE,
3+
IMutationTree,
54
IS_PROXY,
65
ITrackStateTree,
6+
ProxyStateTree,
77
TrackStateTree,
8-
IMutationTree,
9-
MutationTree,
108
} from 'proxy-state-tree'
119

1210
import { EventType, Events } from './internalTypes'
1311

12+
export const IS_DERIVED = Symbol('IS_DERIVED')
13+
1414
export class Derived {
1515
private isDirty: boolean = true
1616
private trackStateTree: ITrackStateTree<any>
@@ -28,12 +28,12 @@ export class Derived {
2828
}
2929
}
3030

31+
boundEvaluate[IS_DERIVED] = true
32+
3133
return boundEvaluate
3234
}
3335
private runScope(tree, path) {
34-
const pathAsArray = path.split('.')
35-
pathAsArray.pop()
36-
const parent = pathAsArray.reduce((curr, key) => curr[key], tree.state)
36+
const parent = path.slice(0, path.length - 1).reduce((curr, key) => curr[key], tree.state)
3737

3838
return this.cb(parent, tree.state)
3939
}
@@ -46,6 +46,12 @@ export class Derived {
4646
if (!this.disposeOnMutation) {
4747
this.disposeOnMutation = proxyStateTree.onMutation(
4848
(_, paths, flushId) => {
49+
// It has been removed from the tree
50+
if (typeof path.reduce((aggr, key) => aggr && aggr[key], proxyStateTree.sourceState) !== 'function') {
51+
this.disposeOnMutation()
52+
return
53+
}
54+
4955
if (this.isDirty) {
5056
return
5157
}

packages/node_modules/overmind/src/index.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
VALUE,
1414
} from 'proxy-state-tree'
1515

16+
import { Derived, IS_DERIVED } from './derived'
1617
import { Devtools, DevtoolsMessage } from './Devtools'
1718
import {
1819
DefaultMode,
@@ -72,11 +73,13 @@ export { SERIALIZE, rehydrate } from './rehydrate'
7273

7374
export { Statemachine, statemachine } from './statemachine'
7475

76+
export const derived = <D extends IDerive<any, any, any>>(cb: D): D extends IDerive<any, any, infer U> ? U : never => cb as any
77+
7578
/** This type can be overwriten by app developers if they want to avoid
7679
* typing and then they can import `Action`, `Operation` etc. directly from
7780
* overmind.
7881
*/
79-
export interface Config {}
82+
export interface Config extends IConfiguration {}
8083

8184
export interface Context extends IContext<Config> {}
8285

@@ -187,7 +190,6 @@ export class Overmind<ThisConfig extends IConfiguration>
187190
private nextExecutionId: number = 0
188191
private mode: DefaultMode | TestMode | SSRMode
189192
private originalConfiguration
190-
private derivedReferences: any[] = []
191193
initialized: Promise<any>
192194
eventHub: EventEmitter<Events>
193195
devtools: Devtools
@@ -371,8 +373,19 @@ export class Overmind<ThisConfig extends IConfiguration>
371373
this.getState(configuration) as any,
372374
{
373375
devmode,
374-
dynamicWrapper: (tree, path, func) =>
375-
func(eventHub, tree, proxyStateTree, path),
376+
onFunction: (tree, path, func) => {
377+
if (func[IS_DERIVED]) {
378+
return { func, value: func(eventHub, tree, proxyStateTree, path.split('.')) }
379+
}
380+
381+
const derived = new Derived(func) as any
382+
383+
return {
384+
func: derived,
385+
value: derived(eventHub, tree, proxyStateTree, path.split('.'))
386+
}
387+
},
388+
376389
onGetter: devmode
377390
? (path, value) => {
378391
this.eventHub.emitAsync(EventType.GETTER, {
@@ -817,9 +830,6 @@ export class Overmind<ThisConfig extends IConfiguration>
817830
if (configuration.state) {
818831
state = processState(
819832
configuration.state,
820-
process.env.NODE_ENV === 'development'
821-
? this.derivedReferences
822-
: undefined
823833
)
824834
}
825835

@@ -911,10 +921,6 @@ export class Overmind<ThisConfig extends IConfiguration>
911921
return this.proxyStateTree.onFlush(cb)
912922
}
913923
reconfigure(configuration: IConfiguration) {
914-
this.derivedReferences.forEach((derived) => {
915-
derived.dispose()
916-
})
917-
this.derivedReferences.length = 0
918924
const mergedConfiguration = {
919925
...configuration,
920926
state: mergeState(

packages/node_modules/overmind/src/utils.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function isPromise(maybePromise: any) {
3535
)
3636
}
3737

38-
export function processState(state: {}, derivedReferences?: any[]) {
38+
export function processState(state: {}) {
3939
return Object.keys(state).reduce((aggr, key) => {
4040
if (key === '__esModule') {
4141
return aggr
@@ -51,13 +51,7 @@ export function processState(state: {}, derivedReferences?: any[]) {
5151
const value = state[key]
5252

5353
if (isPlainObject(value)) {
54-
aggr[key] = processState(value, derivedReferences)
55-
} else if (typeof value === 'function') {
56-
aggr[key] = new Derived(value)
57-
58-
if (derivedReferences) {
59-
derivedReferences.push(aggr[key])
60-
}
54+
aggr[key] = processState(value)
6155
} else {
6256
Object.defineProperty(aggr, key, originalDescriptor as any)
6357
}

packages/node_modules/proxy-state-tree/src/Proxyfier.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -301,13 +301,18 @@ export class Proxifier {
301301
if (typeof targetValue === 'function' && isClass(target)) {
302302
return (...args) => targetValue.call(proxy, ...args)
303303
} else if (typeof targetValue === 'function') {
304-
return proxifier.tree.master.options.dynamicWrapper
305-
? proxifier.tree.master.options.dynamicWrapper(
306-
trackingTree || proxifier.tree,
307-
nestedPath,
308-
targetValue
309-
)
310-
: targetValue.call(target, proxifier.tree, nestedPath)
304+
if (proxifier.tree.master.options.onFunction) {
305+
const { func, value } = proxifier.tree.master.options.onFunction(
306+
trackingTree || proxifier.tree,
307+
nestedPath,
308+
targetValue
309+
)
310+
311+
target[prop] = func
312+
313+
return value
314+
}
315+
return targetValue.call(target, proxifier.tree, nestedPath)
311316
} else {
312317
currentTree.trackPathListeners.forEach((cb) => cb(nestedPath))
313318
trackingTree && trackingTree.proxifier.trackPath(nestedPath)
@@ -353,10 +358,6 @@ export class Proxifier {
353358
objectChangePath
354359
)
355360

356-
if (typeof value === 'function') {
357-
return Reflect.set(target, prop, () => value)
358-
}
359-
360361
return Reflect.set(target, prop, value)
361362
},
362363
deleteProperty(target, prop) {

packages/node_modules/proxy-state-tree/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export interface ITrackStateTree<T extends object> {
7272

7373
export interface IOptions {
7474
devmode?: boolean
75-
dynamicWrapper?: Function
75+
onFunction?: (...args: any[]) => { func: Function, value: any }
7676
onGetter?: Function
7777
}
7878

0 commit comments

Comments
 (0)