Skip to content

Commit

Permalink
feat(overmind): reintroduce base state and explicit state transitions…
Browse files Browse the repository at this point in the history
… for better typing and predicta
  • Loading branch information
christianalfoni committed Oct 16, 2020
1 parent 9694ee2 commit 6529a8b
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 22 deletions.
85 changes: 82 additions & 3 deletions packages/node_modules/overmind/src/statemachine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,38 @@ describe('Statemachine', () => {
expect(overmind.state.current).toBe('FOO')
})

test('should set base state', () => {

type States = {
current: 'FOO'
} | {
current: 'BAR'
}

type BaseState = {
foo: string
}

const state = statemachine<States, never, BaseState>({
FOO: ['BAR'],
BAR: ['FOO']
}).create({
current: 'FOO',
}, {
foo: 'bar'
})


const config = {
state,
}

const overmind = createOvermindMock(config)

expect(overmind.state.current).toBe('FOO')
expect(overmind.state.foo).toBe('bar')
})


test('should transition state', () => {
type States = {
Expand All @@ -41,7 +73,7 @@ describe('Statemachine', () => {
}

const state = statemachine<States, Events>({
TOGGLE: (state) => state.current === 'FOO' ? 'BAR' : 'FOO'
TOGGLE: (state) => ({ current: state.current === 'FOO' ? 'BAR' : 'FOO' })
}).create({
current: 'FOO'
})
Expand All @@ -65,6 +97,47 @@ describe('Statemachine', () => {
expect(overmind.state.current).toBe('BAR')
})

test('should remove state when transitioning', () => {
type States = {
current: 'FOO'
foo: string
} | {
current: 'BAR'
}

type Events = {
type: 'TOGGLE',
}

const state = statemachine<States, Events>({
TOGGLE: () => ({ current: 'BAR' })
}).create({
current: 'FOO',
foo: 'bar'
})
const transition: Action = ({ state }) => {
state.send('TOGGLE')
}

const config = {
state,
actions: {
transition
}
}

interface Action extends IAction<typeof config, void, void> {}

const overmind = createOvermindMock(config)


expect(overmind.state.current).toBe('FOO')
expect(overmind.state.matches('FOO')?.foo).toBe('bar')
overmind.actions.transition()
expect(overmind.state.current).toBe('BAR')
expect((overmind.state as any).foo).toBe(undefined)
})

test('should block mutations in strict mode', () => {
type States = {
current: 'FOO'
Expand All @@ -78,7 +151,13 @@ describe('Statemachine', () => {
}

const state = statemachine<States, Events>({
TOGGLE: (state) => state.current === 'FOO' ? 'BAR' : 'FOO'
TOGGLE: (state) => {
if (state.current === 'FOO') {
return { current: 'BAR' }
}

return { current: 'FOO', foo: 'bar'}
}
}).create({
current: 'FOO',
foo: 'bar'
Expand Down Expand Up @@ -156,7 +235,7 @@ describe('Statemachine', () => {
}

const state = statemachine<States, Events>({
TOGGLE: () => 'BAR'
TOGGLE: () => ({ current: 'BAR' })
}).create({
current: 'FOO'
})
Expand Down
53 changes: 34 additions & 19 deletions packages/node_modules/overmind/src/statemachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { IState } from '.'
type TState = {
current: string
} & {
[key: string]: IState | Statemachine<any, any>
[key: string]: IState | Statemachine<any, any, any>
}

type TEvents = {
Expand All @@ -16,33 +16,36 @@ type TEvents = {


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

export interface MachineMethods<States extends TState, Events extends TEvents> {
export interface MachineMethods<States extends TState, Events extends TEvents, BaseState extends IState> {
matches<T extends States["current"]>(
current: T,
): Statemachine<States extends { current: T} ? States : never, Events> | undefined
): Statemachine<States extends { current: T} ? States : never, Events, BaseState> | undefined
send<T extends Events["type"]>(
...args: Events extends { type: T, data: any } ? [T, Events["data"]] : [T]
): Statemachine<States, Events>
): Statemachine<States, Events, BaseState>
}

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

const INITIAL_STATE = Symbol('INITIAL_STATE')
const TRANSITIONS = Symbol('TRANSITIONS')
const STATE = Symbol('STATE')
const IS_DISPOSED = Symbol('IS_DISPOSED')
const CURRENT_KEYS = Symbol('CURRENT_KEYS')
const BASE_STATE = Symbol('BASE_STATE')

export class StateMachine<State extends TState, Events extends TEvents> {
export class StateMachine<State extends TState, Events extends TEvents, BaseState extends IState> {
current: State["current"]
private [INITIAL_STATE]: State["current"]
private [TRANSITIONS]: StatemachineTransitions<State, Events>
private [STATE]: any
private [BASE_STATE]: BaseState
private [IS_DISPOSED] = false
private clone() {
return new StateMachine(this[TRANSITIONS], deepCopy(this[STATE]))
return new StateMachine(this[TRANSITIONS], deepCopy(this[STATE]), deepCopy(this[BASE_STATE]))
}
private dispose() {
Object.keys(this[VALUE]).forEach((key) => {
Expand All @@ -52,11 +55,14 @@ export class StateMachine<State extends TState, Events extends TEvents> {
})
this[VALUE][IS_DISPOSED] = true
}
constructor(transitions: StatemachineTransitions<State, Events>, state: State) {
constructor(transitions: StatemachineTransitions<State, Events>, state: State, baseState: BaseState) {
this[STATE] = state
this[BASE_STATE] = baseState
this[INITIAL_STATE] = state.current
this[BASE_STATE] = baseState
this[TRANSITIONS] = transitions
Object.assign(this, state)
this[CURRENT_KEYS] = Object.keys(state)
Object.assign(this, state, baseState)
}
send(type, data) {
if (this[VALUE][IS_DISPOSED]) {
Expand All @@ -66,14 +72,21 @@ export class StateMachine<State extends TState, Events extends TEvents> {
return this
}

const existingState = this.current
const tree = (this[PROXY_TREE].master.mutationTree || this[PROXY_TREE])
const transition = this[VALUE][TRANSITIONS][type]

tree.enableMutations()
const result = transition(this, data)

this.current = result || existingState

if (result) {
this[VALUE][CURRENT_KEYS].forEach((key) => {
if (key !== 'current') {
delete this[key]
}
})
this[VALUE][CURRENT_KEYS] = Object.keys(result)
Object.assign(this, result)
}

tree.blockMutations()

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

export type StatemachineFactory<States extends TState, Events extends TEvents> = {
create(state: States): Statemachine<States, Events>
export type StatemachineFactory<States extends TState, Events extends TEvents, BaseState extends IState> = [BaseState] extends [never] ? {
create(state: States): Statemachine<States, Events, {}>
} : {
create(state: States, baseState: BaseState): Statemachine<States, Events, BaseState>
}

export function statemachine<States extends TState, Events extends TEvents = never>(transitions: StatemachineTransitions<States, Events>): StatemachineFactory<States, Events> {
export function statemachine<States extends TState, Events extends TEvents = never, BaseState extends IState = never>(transitions: StatemachineTransitions<States, Events>): StatemachineFactory<States, Events, BaseState> {
return {
create(state) {
return new StateMachine(transitions, state as any) as any
create(state, baseState) {
return new StateMachine(transitions, state as any, baseState as any)
}
}
} as any
}

0 comments on commit 6529a8b

Please sign in to comment.