Create an observable that emits each time a specific transition is taken.
This "flip" machine keeps tossing the coin each time it lands on head. The machine reaches its final state when the side is tail:
stateDiagram-v2
state after_flip <<choice>>
[*] --> flip
flip --> after_flip
after_flip --> head
head --> flip
after_flip --> tail
tail --> [*]
We want to display how many times the coin landed on head so far each time the machine makes a transition to head
e.g.,
flipping…
head. flip again.
current head count: 1
head. flip again.
current head count: 2
head. flip again.
current head count: 3
head. flip again.
current head count: 4
tail. stop.
If the coin lands on tail on the first throw:
flipping…
tail. stop.
The flip machine can be implemented as follow:
const flip = interpret(createMachine({
initial: 'flip',
states: {
flip: {
always: [
{target: 'head', cond: () => Math.random() < 0.8},
{target: 'tail'}
]
},
head: {
entry: log(() => 'head. flip again.'),
after: {
50: {
target: 'flip'
}
}
},
tail: {
type: 'final',
entry: log(() => 'tail. stop.')
}
}
}));
We'll start by creating an observable that emits when the machine reaches its final state.
const tail$ =
fromEventPattern(
// how to get values
handler => flip.onDone(handler),
// how to stop getting values
handler => flip.off(handler))
.pipe(take(1) /* <- unsubscribe after first emission */);
Note: We don't actually care about what that observable emits. We just use it as a signal to unsubscribe from the other observable.
Now let's create an observable that emits each time the machine takes a transition to head
:
const head$ =
fromEventPattern(
// how to get values
handler => {
flip.onTransition(st => {
if (st.matches('head')) {
handler(1); // <- value emitted by the observable. see `n` below
}
});
},
// how to stop getting values
handler => flip.off(handler))
.pipe(
scan((tot, n) => tot + n, 0),
takeUntil(tail$) /* <- unsubscribe as soon as `tail$` emits */);
We can now run the machine and subscribe to head$
:
console.log('flipping…');
flip.start();
head$.subscribe(tot => {
console.log(`current head count: ${tot}`);
});
import { createMachine, interpret } from "xstate";
import { fromEventPattern, scan, take, takeUntil } from "rxjs";
import { log } from "xstate/lib/actions.js";
const flip = interpret(createMachine({
initial: 'flip',
states: {
flip: {
always: [
{target: 'head', cond: () => Math.random() < 0.8},
{target: 'tail'}
]
},
head: {
entry: log(() => 'head. flip again.'),
after: {
50: {
target: 'flip'
}
}
},
tail: {
type: 'final',
entry: log(() => 'tail. stop.')
}
}
}));
const tail$ =
fromEventPattern(
// how to get values
handler => flip.onDone(handler),
// how to stop getting values
handler => flip.off(handler))
.pipe(take(1) /* <- unsubscribe after first emission */);
const head$ =
fromEventPattern(
// how to get values
handler => {
flip.onTransition(st => {
if (st.matches('head')) {
handler(1); // <- value emitted by the observable. see `n` below
}
});
},
// how to stop getting values
handler => flip.off(handler))
.pipe(
scan((tot, n) => tot + n, 0),
takeUntil(tail$) /* unsubscribe as soon as `tail$` emits */);
console.log('flipping…');
flip.start();
head$.subscribe(tot => {
console.log(`current head count: ${tot}`);
});