Skip to content

Commit da62923

Browse files
feat(overmind): new transition API and allow async exit transitions
BREAKING CHANGE: explicit transition API
1 parent ed5c923 commit da62923

File tree

2 files changed

+94
-37
lines changed

2 files changed

+94
-37
lines changed

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: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,19 @@ export interface MachineMethods<States extends TStates> {
1515
matches<T extends States["state"][]>(...states: T): this is Statemachine<States> & (States extends {
1616
state: T extends Array<infer S> ? S : never;
1717
} ? 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> = States & 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

2933
class StateMachine<States extends TStates> {
@@ -33,31 +37,36 @@ class StateMachine<States extends TStates> {
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)