Skip to content

Commit ea68e1b

Browse files
feat(overmind): reintroduce base state and explicit state transitions for better typing and predicta
1 parent b1a7678 commit ea68e1b

File tree

2 files changed

+116
-22
lines changed

2 files changed

+116
-22
lines changed

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

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,38 @@ describe('Statemachine', () => {
2828
expect(overmind.state.current).toBe('FOO')
2929
})
3030

31+
test('should set base state', () => {
32+
33+
type States = {
34+
current: 'FOO'
35+
} | {
36+
current: 'BAR'
37+
}
38+
39+
type BaseState = {
40+
foo: string
41+
}
42+
43+
const state = statemachine<States, never, BaseState>({
44+
FOO: ['BAR'],
45+
BAR: ['FOO']
46+
}).create({
47+
current: 'FOO',
48+
}, {
49+
foo: 'bar'
50+
})
51+
52+
53+
const config = {
54+
state,
55+
}
56+
57+
const overmind = createOvermindMock(config)
58+
59+
expect(overmind.state.current).toBe('FOO')
60+
expect(overmind.state.foo).toBe('bar')
61+
})
62+
3163

3264
test('should transition state', () => {
3365
type States = {
@@ -41,7 +73,7 @@ describe('Statemachine', () => {
4173
}
4274

4375
const state = statemachine<States, Events>({
44-
TOGGLE: (state) => state.current === 'FOO' ? 'BAR' : 'FOO'
76+
TOGGLE: (state) => ({ current: state.current === 'FOO' ? 'BAR' : 'FOO' })
4577
}).create({
4678
current: 'FOO'
4779
})
@@ -65,6 +97,47 @@ describe('Statemachine', () => {
6597
expect(overmind.state.current).toBe('BAR')
6698
})
6799

100+
test('should remove state when transitioning', () => {
101+
type States = {
102+
current: 'FOO'
103+
foo: string
104+
} | {
105+
current: 'BAR'
106+
}
107+
108+
type Events = {
109+
type: 'TOGGLE',
110+
}
111+
112+
const state = statemachine<States, Events>({
113+
TOGGLE: () => ({ current: 'BAR' })
114+
}).create({
115+
current: 'FOO',
116+
foo: 'bar'
117+
})
118+
const transition: Action = ({ state }) => {
119+
state.send('TOGGLE')
120+
}
121+
122+
const config = {
123+
state,
124+
actions: {
125+
transition
126+
}
127+
}
128+
129+
interface Action extends IAction<typeof config, void, void> {}
130+
131+
const overmind = createOvermindMock(config)
132+
133+
134+
expect(overmind.state.current).toBe('FOO')
135+
expect(overmind.state.matches('FOO')?.foo).toBe('bar')
136+
overmind.actions.transition()
137+
expect(overmind.state.current).toBe('BAR')
138+
expect((overmind.state as any).foo).toBe(undefined)
139+
})
140+
68141
test('should block mutations in strict mode', () => {
69142
type States = {
70143
current: 'FOO'
@@ -78,7 +151,13 @@ describe('Statemachine', () => {
78151
}
79152

80153
const state = statemachine<States, Events>({
81-
TOGGLE: (state) => state.current === 'FOO' ? 'BAR' : 'FOO'
154+
TOGGLE: (state) => {
155+
if (state.current === 'FOO') {
156+
return { current: 'BAR' }
157+
}
158+
159+
return { current: 'FOO', foo: 'bar'}
160+
}
82161
}).create({
83162
current: 'FOO',
84163
foo: 'bar'
@@ -156,7 +235,7 @@ describe('Statemachine', () => {
156235
}
157236

158237
const state = statemachine<States, Events>({
159-
TOGGLE: () => 'BAR'
238+
TOGGLE: () => ({ current: 'BAR' })
160239
}).create({
161240
current: 'FOO'
162241
})

packages/node_modules/overmind/src/statemachine.ts

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { IState } from '.'
66
type TState = {
77
current: string
88
} & {
9-
[key: string]: IState | Statemachine<any, any>
9+
[key: string]: IState | Statemachine<any, any, any>
1010
}
1111

1212
type TEvents = {
@@ -16,33 +16,36 @@ type TEvents = {
1616

1717

1818
export type StatemachineTransitions<States extends TState, Events extends TEvents> = {
19-
[Type in Events["type"]]: ((state: States, payload: Events extends { type: Type } ? Events["data"] : never) => States["current"] | void)
19+
[Type in Events["type"]]: ((state: States, payload: Events extends { type: Type } ? Events["data"] : never) => States | void)
2020
}
2121

22-
export interface MachineMethods<States extends TState, Events extends TEvents> {
22+
export interface MachineMethods<States extends TState, Events extends TEvents, BaseState extends IState> {
2323
matches<T extends States["current"]>(
2424
current: T,
25-
): Statemachine<States extends { current: T} ? States : never, Events> | undefined
25+
): Statemachine<States extends { current: T} ? States : never, Events, BaseState> | undefined
2626
send<T extends Events["type"]>(
2727
...args: Events extends { type: T, data: any } ? [T, Events["data"]] : [T]
28-
): Statemachine<States, Events>
28+
): Statemachine<States, Events, BaseState>
2929
}
3030

31-
export type Statemachine<States extends TState, Events extends TEvents> = States & MachineMethods<States, Events>
31+
export type Statemachine<States extends TState, Events extends TEvents, BaseState extends IState> = States & BaseState & MachineMethods<States, Events, BaseState>
3232

3333
const INITIAL_STATE = Symbol('INITIAL_STATE')
3434
const TRANSITIONS = Symbol('TRANSITIONS')
3535
const STATE = Symbol('STATE')
3636
const IS_DISPOSED = Symbol('IS_DISPOSED')
37+
const CURRENT_KEYS = Symbol('CURRENT_KEYS')
38+
const BASE_STATE = Symbol('BASE_STATE')
3739

38-
export class StateMachine<State extends TState, Events extends TEvents> {
40+
export class StateMachine<State extends TState, Events extends TEvents, BaseState extends IState> {
3941
current: State["current"]
4042
private [INITIAL_STATE]: State["current"]
4143
private [TRANSITIONS]: StatemachineTransitions<State, Events>
4244
private [STATE]: any
45+
private [BASE_STATE]: BaseState
4346
private [IS_DISPOSED] = false
4447
private clone() {
45-
return new StateMachine(this[TRANSITIONS], deepCopy(this[STATE]))
48+
return new StateMachine(this[TRANSITIONS], deepCopy(this[STATE]), deepCopy(this[BASE_STATE]))
4649
}
4750
private dispose() {
4851
Object.keys(this[VALUE]).forEach((key) => {
@@ -52,11 +55,14 @@ export class StateMachine<State extends TState, Events extends TEvents> {
5255
})
5356
this[VALUE][IS_DISPOSED] = true
5457
}
55-
constructor(transitions: StatemachineTransitions<State, Events>, state: State) {
58+
constructor(transitions: StatemachineTransitions<State, Events>, state: State, baseState: BaseState) {
5659
this[STATE] = state
60+
this[BASE_STATE] = baseState
5761
this[INITIAL_STATE] = state.current
62+
this[BASE_STATE] = baseState
5863
this[TRANSITIONS] = transitions
59-
Object.assign(this, state)
64+
this[CURRENT_KEYS] = Object.keys(state)
65+
Object.assign(this, state, baseState)
6066
}
6167
send(type, data) {
6268
if (this[VALUE][IS_DISPOSED]) {
@@ -66,14 +72,21 @@ export class StateMachine<State extends TState, Events extends TEvents> {
6672
return this
6773
}
6874

69-
const existingState = this.current
7075
const tree = (this[PROXY_TREE].master.mutationTree || this[PROXY_TREE])
7176
const transition = this[VALUE][TRANSITIONS][type]
7277

7378
tree.enableMutations()
7479
const result = transition(this, data)
75-
76-
this.current = result || existingState
80+
81+
if (result) {
82+
this[VALUE][CURRENT_KEYS].forEach((key) => {
83+
if (key !== 'current') {
84+
delete this[key]
85+
}
86+
})
87+
this[VALUE][CURRENT_KEYS] = Object.keys(result)
88+
Object.assign(this, result)
89+
}
7790

7891
tree.blockMutations()
7992

@@ -86,14 +99,16 @@ export class StateMachine<State extends TState, Events extends TEvents> {
8699
}
87100
}
88101

89-
export type StatemachineFactory<States extends TState, Events extends TEvents> = {
90-
create(state: States): Statemachine<States, Events>
102+
export type StatemachineFactory<States extends TState, Events extends TEvents, BaseState extends IState> = [BaseState] extends [never] ? {
103+
create(state: States): Statemachine<States, Events, {}>
104+
} : {
105+
create(state: States, baseState: BaseState): Statemachine<States, Events, BaseState>
91106
}
92107

93-
export function statemachine<States extends TState, Events extends TEvents = never>(transitions: StatemachineTransitions<States, Events>): StatemachineFactory<States, Events> {
108+
export function statemachine<States extends TState, Events extends TEvents = never, BaseState extends IState = never>(transitions: StatemachineTransitions<States, Events>): StatemachineFactory<States, Events, BaseState> {
94109
return {
95-
create(state) {
96-
return new StateMachine(transitions, state as any) as any
110+
create(state, baseState) {
111+
return new StateMachine(transitions, state as any, baseState as any)
97112
}
98-
}
113+
} as any
99114
}

0 commit comments

Comments
 (0)