Closed davidkpiano closed 5 years ago
Looks great. I guess there would be an on('start', ...)
and on('stop', ...)
whenever an activity is started or stopped (given the activity proposal). I love the fact that this ends up hiding the delayed events. Such a runner will make it a lot easier to start using xstate.
Come to think of it, starting and stopping activities might be better handled by being able to specify the activity to start/stop in the handler:
interpreter.onStart('http_request', () => xhr.start());
interpreter.onStop('http_request', () => xhr.cancel());
interpreter.onStart('loading', (e) => this.addClass(e)); // e would be the string 'loading'
interpreter.onStop('loading', this.removeClass);
Yeah @mogsie that's a good idea. For desugaring, I'd also like to be compatible with the (proposed) Observable spec so that for all statechart-specific events:
const interpreter = Interpreter(machine); // implements Observable interface
// This will also .init() the interpereter
const subscription = interpreter.subscribe(statechartEvent => {
// all statechart events will be passed into the subscription
});
// unsubscribe
subscription.unsubscribe();
So interpreter.onStart(...)
would be sugar for interpreter.filter(e => e.type === 'start')
or something similar. But yes, "throughout" activities would work just as you've described.
Cool so with observables we can compose. If we think abstractly a statechart will take as inputs events, and will output actions (activities can simply be "start X" actions).
We could handle this with functions which take a stream of actions and return a stream of events. Actions in, events out. So these functions will take the output of a statechart (action) and give us back something we can feed back into the statechart (events).
function handleHttpRequest(action$) {
return action$.filter(action => action.start === 'http_request')
.mergeMap(() =>
ajax('http://google.com') // perform the actual http request
.mapTo({ type: 'HTTP_SUCCESS' }) // send this event to the statechart
// note how we can stop the observable (takeUntil) when a "stop http_request" is output
.takeUntil(action$.filter(action => action.stop === 'http_request'))
)
}
function handleLoading(action$) {
return action$.filter(action => action.start === 'loading')
.mergeMap(() =>
// can return an object that is passed to setState()
setState({ loading: true })
.mergeMap(
action$.filter(action => action.stop === 'loading')
.mapTo({ loading: false })
)
)
}
statechart.subscribe(
handleHttpRequest,
handleLoading
)
I'm just bikeshedding my thoughts here 🤔
Great idea - the .subscribe
syntax doesn't follow the Observable spec though (if it took 3 arguments, it'd be onNext
, onError
, and onComplete
). But you brought up a good point of needing a way to "input" actions, so Interpreter(...)
can take two arguments:
machine: Machine
- the statechart machine to useactions: Iterable
- in this case, it's an Observable stream of actions, but it can just as well be an array or generatorTo implement the feedback loop, I'd recommend using either a Subject
(with interpreter.subscribe(subject$)
) or the expand(...)
operator (with using machine.transition
inside of the scan(...)
operator).
Here's how it would look with the above changes:
const machineEvent$ = new Rx.Subject();
const event$ = // ... all the external events
const allEvent$ = merge(event$, machineEvent$); // RxJS merge creator
const state$ = Interpreter(machine, allEvent$); // hot observable if allEvent$ is hot
state$.subscribe(machineEvent$); // feedback loop
state$.subscribe(state => {
render(state); // render UI based on state
}
Note that right now, you don't need a special Interpreter
to have a statechart interpreted with RxJS - you can use the scan(...)
operator:
const action$ = // ...
const state$ = action$.pipe(
// or just .scan(machine.transition)
scan((state, action) => machine.transition(state, action), machine.initialState)
);
I'm going to play around with using the expand(...)
operator to do a feedback loop - might be more complicated than just using Interpreter(...)
and passing the "feedback" proxy Subject in.
(just jotting down ideas too - Observables seem like the best pattern to use with statecharts)
This will be done in the xstate-interpreter
package.
Love the ideas in this feature request, is it still planned?
It is, yes.
Is there a way currently to observe state changes? (xstate-interpreter
doesn't look done). I'm looking at this as a solution to coordinate state changes between different components. It seems like being able to observe state changes and then do side-effect-full things is pretty core to getting work done with a state machine.... but maybe there's a better way?
There will be. What are the use-cases you're specifically looking for? E.g.,
And what's your ideal API for how this would look? There's currently an interpreter in the main
branch (not done yet), so the above might look like:
const interpreter = interpret(machine);
// when the state changes
interpreter.onTransition(nextState => ...);
// TENTATIVE
// when extended state changes
interpreter.onChange(extState => ...);
// when a specific value in extended state changes
interpreter.onChange('someProp', somePropValue => ...);
// when an action is fired
interpreter.onAction(action => ...);
// when a specific action is fired
interpreter.onAction('focusInput', focusInputAction => ...);
// when an activity starts/stops
interpreter.onActivityStart('someActivity', activityAction => ...);
interpreter.onActivityStop('someActivity', activityAction => ...);
// when the machine reaches a final state
interpreter.onDone(finalState => ...);
@davidkpiano sorry for the late response -- I'm hoping to observe finite state changes + the action that transitioned them to that state, so it looks like the API you have there would cover that.
Closing - the interpreter is feature-complete for 4.0 (though more features may be added for 4.x)
Related to #36 cc. @mogsie
Provide a separate interpreter which will act as a simple event emitter for which to run a machine. This is technically side-effectful, but it is not a core part of how
xstate
works (re. thetransition
function) nor is it required. The developer should always be allowed to implement their own runner.Proposed API
Interpreter(machine: Machine)
returns an event emitter:interpreter.on(eventName: string, callback: (state: State) => void)
interpreter.onTransition(callback)
, etc.interpreter.off(callback)
removes all listeners for that callbackinterpreter.dispatch(event: Event)
dispatches events (object/string) to machineinterpreter.init()
starts the interpreter on the initial stateRelated to the proposed delayed actions syntax, we could have something like:
Usage with a reactive library, such as RxJS:
Thoughts? Very open to feedback on this one.