Skip to content

Commit 5b55c1b

Browse files
Merge branch 'next' of https://github.com/cerebral/overmind into next
2 parents 3d16a7a + c59849a commit 5b55c1b

File tree

3 files changed

+101
-46
lines changed

3 files changed

+101
-46
lines changed

packages/node_modules/overmind/src/config/merge.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export function merge(...configurations: IConfiguration[]): IConfiguration {
131131
config.onInitialize ? aggr.concat(config.onInitialize) : aggr,
132132
[] as any[]
133133
)
134-
134+
const rootConfiguration = configurations.shift()
135135
const reducedConfigurations = configurations.reduce(
136136
(aggr, config) => {
137137
const stateDuplicates = aggr.state
@@ -178,13 +178,11 @@ export function merge(...configurations: IConfiguration[]): IConfiguration {
178178
}
179179
},
180180
{
181+
...rootConfiguration,
181182
onInitialize: initializers.length
182183
? (context, value) =>
183184
Promise.all(initializers.map((cb) => cb(context, value)))
184185
: undefined,
185-
state: {},
186-
effects: {},
187-
actions: {},
188186
}
189187
)
190188
return reducedConfigurations

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

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('Statemachine', () => {
4141
state: 'FOO'
4242
})
4343
const transition: Action = ({ state }) => {
44-
state.BAR()
44+
state.transition('BAR')
4545
}
4646

4747
const config = {
@@ -72,7 +72,7 @@ describe('Statemachine', () => {
7272
state: 'FOO'
7373
})
7474
const transition: Action = ({ state }) => {
75-
state.BAR()
75+
state.transition('BAR')
7676
}
7777

7878
const config = {
@@ -88,7 +88,7 @@ describe('Statemachine', () => {
8888
overmind.actions.transition()
8989
expect(overmind.state.state).toBe('FOO')
9090
})
91-
test('should run entry and exit transition', () => {
91+
test('should run entry and exit transition', async () => {
9292
expect.assertions(3)
9393
type States = {
9494
state: 'FOO'
@@ -104,9 +104,9 @@ describe('Statemachine', () => {
104104
})
105105

106106
const transition: Action = ({ state }) => {
107-
state.BAR(() => {
107+
return state.transition('BAR', () => {
108108
expect(state.state).toBe('BAR')
109-
state.FOO()
109+
state.transition('FOO')
110110
}, () => {
111111
expect(state.state).toBe('BAR')
112112
})
@@ -119,10 +119,10 @@ describe('Statemachine', () => {
119119
}
120120
}
121121

122-
interface Action extends IAction<typeof config, void, void> {}
122+
interface Action extends IAction<typeof config, void, void | Promise<void>> {}
123123

124124
const overmind = createOvermindMock(config)
125-
overmind.actions.transition()
125+
await overmind.actions.transition()
126126
expect(overmind.state.state).toBe('FOO')
127127
})
128128
test('should flush changes to transitions', () => {
@@ -142,7 +142,7 @@ describe('Statemachine', () => {
142142
})
143143

144144
const transition: Action = ({ state }) => {
145-
state.BAR()
145+
state.transition('BAR')
146146
}
147147

148148
const config = {
@@ -177,7 +177,7 @@ describe('Statemachine', () => {
177177
state: 'FOO'
178178
})
179179
const transition: Action = ({ state }) => {
180-
return state.BAR(async () => {
180+
return state.transition('BAR', async () => {
181181
await Promise.resolve()
182182
expect(state[PROXY_TREE].master.mutationTree.isBlocking).toBe(true)
183183
})
@@ -214,7 +214,7 @@ describe('Statemachine', () => {
214214
})
215215

216216
const transition: Action = ({ state }) => {
217-
state.BAR()
217+
state.transition('BAR')
218218
}
219219

220220
const config = {
@@ -252,8 +252,8 @@ describe('Statemachine', () => {
252252
})
253253

254254
const transition: Action = ({ state }) => {
255-
state.BAR(() => {
256-
state.FOO((current) => {
255+
return state.transition('BAR', () => {
256+
state.transition('FOO', (current) => {
257257
current.foo = 'bar2'
258258
})
259259
}, (current) => {
@@ -268,7 +268,7 @@ describe('Statemachine', () => {
268268
}
269269
}
270270

271-
interface Action extends IAction<typeof config, void, void> {}
271+
interface Action extends IAction<typeof config, void, void | Promise<void>> {}
272272

273273
const overmind = createOvermind(config)
274274
overmind.actions.transition()
@@ -278,4 +278,52 @@ describe('Statemachine', () => {
278278
// @ts-ignore
279279
expect(overmind.state.bar).toBe('baz2')
280280
})
281+
282+
test('should allow async exit transition', async () => {
283+
expect.assertions(3)
284+
285+
type States = {
286+
state: 'FOO'
287+
foo: string
288+
} | {
289+
state: 'BAR'
290+
bar: string
291+
}
292+
293+
const state = statemachine<States>({
294+
FOO: ['BAR'],
295+
BAR: ['FOO']
296+
}, {
297+
state: 'FOO',
298+
foo: 'bar'
299+
})
300+
301+
const transition: Action = ({ state }) => {
302+
return state.transition('BAR', async () => {
303+
await Promise.resolve()
304+
state.transition('FOO', (current) => {
305+
current.foo = 'bar2'
306+
})
307+
}, (current) => {
308+
current.bar = 'baz2'
309+
})
310+
}
311+
312+
const config = {
313+
state,
314+
actions: {
315+
transition
316+
}
317+
}
318+
319+
interface Action extends IAction<typeof config, void, void | Promise<void>> {}
320+
321+
const overmind = createOvermind(config)
322+
await overmind.actions.transition()
323+
expect(overmind.state.state).toBe('FOO')
324+
// @ts-ignore
325+
expect(overmind.state.foo).toBe('bar2')
326+
// @ts-ignore
327+
expect(overmind.state.bar).toBe('baz2')
328+
})
281329
})

packages/node_modules/overmind/src/statemachine.ts

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,52 +12,61 @@ export type StatemachineTransitions<States extends TStates> = {
1212

1313
export interface MachineMethods<States extends TStates> {
1414
reset: () => void
15-
matches<T extends States["state"][]>(...states: T): this is Statemachine<States> & (States extends {
15+
matches<T extends States["state"][]>(...states: T): this is Statemachine<States, States extends {
1616
state: T extends Array<infer S> ? S : never;
17-
} ? States : never);
17+
} ? States : never>;
18+
transition<State extends States["state"]>(
19+
state: State,
20+
entry?: (current: Statemachine<States, States extends { state: State } ? States : never>) => void,
21+
exit?: (current: Statemachine<States, States extends { state: State } ? States : never>) => void
22+
): Promise<void>
1823
whenTransitioned: (state: States["state"]) => Promise<void>
1924
}
2025

21-
export type Statemachine<States extends TStates> = States & MachineMethods<States> & {
22-
[State in States["state"]]: <O>(entry?: (current: Statemachine<States> & (States extends { state: State } ? States : never)) => O, exit?: (current: Statemachine<States> & (States extends { state: State } ? States : never)) => void) => O
23-
}
26+
export type Statemachine<States extends TStates, State extends TStates = States> = State & MachineMethods<States>
2427

2528
const CURRENT_EXIT = Symbol('CURRENT_EXIT')
2629
const INITIAL_STATE = Symbol('INITIAL_STATE')
30+
const TRANSITIONS = Symbol('TRANSITIONS')
2731
const PENDING_TRANSITIONS = Symbol('PENDING_TRANSITIONS')
2832

29-
class StateMachine<States extends TStates> {
30-
state: States["state"]
33+
class StateMachine<States extends TStates, State extends TStates = States> {
34+
state: State["state"]
3135
private [CURRENT_EXIT]: (() => void) | undefined
32-
private [INITIAL_STATE]: States["state"]
36+
private [INITIAL_STATE]: State["state"]
3337
private [PENDING_TRANSITIONS]: { [key: string]: Function[] } = {}
3438
constructor(transitions: StatemachineTransitions<States>, definition: States) {
3539
this[INITIAL_STATE] = definition.state
40+
this[TRANSITIONS] = transitions
3641
Object.assign(this, definition)
42+
}
43+
transition(state, entry, exit) {
44+
const transitions = this[VALUE][TRANSITIONS]
45+
if (transitions[this.state].includes(state)) {
46+
const tree = (this[PROXY_TREE].master.mutationTree || this[PROXY_TREE])
47+
let exitResult
48+
tree.enableMutations()
49+
50+
if (this[CURRENT_EXIT]) this[CURRENT_EXIT]!()
51+
if (exit) {
52+
exitResult = new Promise((resolve) => {
53+
this[VALUE][CURRENT_EXIT] = () => resolve(exit(this))
54+
})
55+
} else {
56+
this[VALUE][CURRENT_EXIT] = undefined
57+
}
58+
this.state = state
59+
const entryResult = entry && entry(this)
3760

38-
Object.keys(transitions).reduce((aggr, key) => {
39-
aggr[key] = function (entry, exit) {
40-
if (transitions[this.state].includes(key as any)) {
41-
const tree = (this[PROXY_TREE].master.mutationTree || this[PROXY_TREE])
42-
43-
tree.enableMutations()
44-
if (this[CURRENT_EXIT]) this[CURRENT_EXIT](this)
45-
this[VALUE][CURRENT_EXIT] = exit
46-
this.state = key as any
47-
const result = entry && entry(this)
48-
tree.blockMutations()
49-
50-
if (this[VALUE][PENDING_TRANSITIONS][this.state]) {
51-
this[VALUE][PENDING_TRANSITIONS][this.state].forEach((resolve) => resolve())
52-
this[VALUE][PENDING_TRANSITIONS][this.state] = []
53-
}
61+
tree.blockMutations()
5462

55-
return result
56-
}
63+
if (this[VALUE][PENDING_TRANSITIONS][this.state]) {
64+
this[VALUE][PENDING_TRANSITIONS][this.state].forEach((resolve) => resolve())
65+
this[VALUE][PENDING_TRANSITIONS][this.state] = []
5766
}
58-
59-
return aggr
60-
}, this)
67+
68+
return exitResult || entryResult
69+
}
6170
}
6271
matches(...states) {
6372
if (states.includes(this.state)) {

0 commit comments

Comments
 (0)