Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): raise null events on root object, but only if machine is not "done". Fixes #754 #832

Merged
merged 4 commits into from
Nov 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions docs/guides/states.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ A `State` object instance is JSON-serializable and has the following properties:
- `activities` - a mapping of [activities](./activities.md) to `true` if the activity started, or `false` if stopped.
- `history` - the previous `State` instance
- `meta` - any static meta data defined on the `meta` property of the [state node](./statenodes.md)
- `done` - whether the state indicates a final state <Badge text="4.7.1" />

It contains other properties such as `historyValue`, `events`, `tree`, and others that are generally not relevant and are used internally.

## State Methods and Getters
## State Methods and Properties

There are some helpful methods and getters that you can use for a better development experience:
There are some helpful methods and properties that you can use for a better development experience:

### `state.matches(parentStateValue)`

Expand All @@ -72,7 +73,7 @@ console.log(state.matches('green'));

### `state.nextEvents`

This getter specifies the next events that will cause a transition from the current state:
This specifies the next events that will cause a transition from the current state:

```js
const { initialState } = lightMachine;
Expand All @@ -85,7 +86,7 @@ This is useful in determining which next events can be taken, and representing t

### `state.changed`

This getter specifies if this `state` has changed from the previous state. A state is considered "changed" if:
This specifies if this `state` has changed from the previous state. A state is considered "changed" if:

- Its value is not equal to its previous value, or:
- It has any new actions (side-effects) to execute.
Expand All @@ -109,6 +110,32 @@ console.log(unchangedState.changed);
// => false
```

### `state.done`

This specifies whether the `state` is a ["final state"](./final.md) - that is, a state that indicates that its machine has reached its final (terminal) state and can no longer transition to any other state.

```js
const answeringMachine = Machine({
initial: 'unanswered',
states: {
unanswered: {
on: {
ANSWER: 'answered'
}
},
answered: {
type: 'final'
}
}
});

const { initialState } = answeringMachine;
initialState.done; // false

const answeredState = answeringMachine.transition(initialState, 'ANSWER');
initialState.done; // true
```

### `state.toStrings()`

This method returns an array of strings that represent _all_ of the state value paths. For example, assuming the current `state.value` is `{ red: 'stop' }`:
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ export class State<
* An initial state (with no history) will return `undefined`.
*/
public changed: boolean | undefined;
/**
* Indicates whether the state is a final state.
*/
public done: boolean | undefined;
/**
* The enabled state nodes representative of the state value.
*/
Expand Down Expand Up @@ -237,6 +241,7 @@ export class State<
this.configuration = config.configuration;
this.transitions = config.transitions;
this.children = config.children;
this.done = !!config.done;

Object.defineProperty(this, 'nextEvents', {
get: () => {
Expand Down
58 changes: 32 additions & 26 deletions packages/core/src/StateNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1258,11 +1258,13 @@ class StateNode<
: ({} as Record<string, Actor>)
);

const stateNodes = resolvedStateValue
? this.getStateNodes(resolvedStateValue)
const resolvedConfiguration = resolvedStateValue
? stateTransition.configuration
: currentState
? currentState.configuration
: [];

const meta = [this, ...stateNodes].reduce(
const meta = resolvedConfiguration.reduce(
(acc, stateNode) => {
if (stateNode.meta !== undefined) {
acc[stateNode.id] = stateNode.meta;
Expand All @@ -1272,6 +1274,8 @@ class StateNode<
{} as Record<string, string>
);

const isDone = isInFinalState(resolvedConfiguration, this);

const nextState = new State<TContext, TEvent, TStateSchema, TState>({
value: resolvedStateValue || currentState!.value,
context: updatedContext,
Expand Down Expand Up @@ -1301,13 +1305,10 @@ class StateNode<
? currentState.meta
: undefined,
events: [],
configuration: resolvedStateValue
? stateTransition.configuration
: currentState
? currentState.configuration
: [],
configuration: resolvedConfiguration,
transitions: stateTransition.transitions,
children
children,
done: isDone
});

nextState.changed =
Expand All @@ -1324,25 +1325,30 @@ class StateNode<
}

let maybeNextState = nextState;
const isTransient = stateNodes.some(stateNode => stateNode._transient);

if (isTransient) {
maybeNextState = this.resolveRaisedTransition(
maybeNextState,
{
type: actionTypes.nullEvent
},
_event
);
}
if (!isDone) {
const isTransient =
davidkpiano marked this conversation as resolved.
Show resolved Hide resolved
this._transient ||
configuration.some(stateNode => stateNode._transient);

if (isTransient) {
maybeNextState = this.resolveRaisedTransition(
maybeNextState,
{
type: actionTypes.nullEvent
},
_event
);
}

while (raisedEvents.length) {
const raisedEvent = raisedEvents.shift()!;
maybeNextState = this.resolveRaisedTransition(
maybeNextState,
raisedEvent._event,
_event
);
while (raisedEvents.length) {
const raisedEvent = raisedEvents.shift()!;
maybeNextState = this.resolveRaisedTransition(
maybeNextState,
raisedEvent._event,
_event
);
}
}

// Detect if state changed
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,7 @@ export interface StateConfig<TContext, TEvent extends EventObject> {
configuration: Array<StateNode<TContext, any, TEvent>>;
transitions: Array<TransitionDefinition<TContext, TEvent>>;
children: Record<string, Actor>;
done?: boolean;
}

export interface StateSchema<TC = any> {
Expand Down
24 changes: 20 additions & 4 deletions packages/core/test/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ type Events =
| { type: 'THREE_EVENT' }
| { type: 'TO_THREE' }
| { type: 'TO_TWO'; foo: string }
| { type: 'TO_TWO_MAYBE' };
| { type: 'TO_TWO_MAYBE' }
| { type: 'TO_FINAL' };

const machine = Machine<any, Events>({
initial: 'one',
states: {
one: {
onEntry: ['enter'],
entry: ['enter'],
on: {
EXTERNAL: {
target: 'one',
Expand All @@ -45,7 +46,8 @@ const machine = Machine<any, Events>({
}
},
TO_THREE: 'three',
FORBIDDEN_EVENT: undefined
FORBIDDEN_EVENT: undefined,
TO_FINAL: 'success'
}
},
two: {
Expand Down Expand Up @@ -95,6 +97,9 @@ const machine = Machine<any, Events>({
on: {
THREE_EVENT: '.'
}
},
success: {
type: 'final'
}
},
on: {
Expand All @@ -108,7 +113,7 @@ describe('State', () => {
expect(machine.initialState.changed).not.toBeDefined();
});

it('states from external transitions with onEntry actions should be changed', () => {
it('states from external transitions with entry actions should be changed', () => {
const changedState = machine.transition(machine.initialState, 'EXTERNAL');
expect(changedState.changed).toBe(true);
});
Expand Down Expand Up @@ -259,6 +264,7 @@ describe('State', () => {
'INERT',
'INTERNAL',
'MACHINE_EVENT',
'TO_FINAL',
'TO_THREE',
'TO_TWO',
'TO_TWO_MAYBE'
Expand Down Expand Up @@ -545,4 +551,14 @@ describe('State', () => {
expect(toStrings()).toEqual(['one']);
});
});

describe('.done', () => {
it('should show that a machine has not reached its final state', () => {
expect(machine.initialState.done).toBeFalsy();
});

it('should show that a machine has reached its final state', () => {
expect(machine.transition(undefined, 'TO_FINAL').done).toBeTruthy();
});
});
});
40 changes: 39 additions & 1 deletion packages/core/test/transient.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Machine } from '../src/index';
import { Machine, createMachine, interpret } from '../src/index';
import { assign, raise } from '../src/actions';

const greetingContext = { hour: 10 };
Expand Down Expand Up @@ -398,4 +398,42 @@ describe('transient states (eventless transitions)', () => {
const state = machine.transition('a', 'FOO');
expect(state.value).toBe('pass');
});

it('should work with transient transition on root', done => {
const machine = createMachine<any, any>({
id: 'machine',
initial: 'first',
context: { count: 0 },
states: {
first: {
on: {
ADD: {
actions: assign({ count: ctx => ctx.count + 1 })
}
}
},
success: {
type: 'final'
}
},
on: {
'': [
{
target: '.success',
Andarist marked this conversation as resolved.
Show resolved Hide resolved
cond: ctx => {
return ctx.count > 0;
}
}
]
}
});

const service = interpret(machine).onDone(() => {
done();
});

service.start();

service.send('ADD');
});
});
Loading