Skip to content

Instantly share code, notes, and snippets.

@nathanhammond
Created August 27, 2020 09:07
Show Gist options
  • Save nathanhammond/f3f6639c3f60e03c787aeeda74389206 to your computer and use it in GitHub Desktop.
Save nathanhammond/f3f6639c3f60e03c787aeeda74389206 to your computer and use it in GitHub Desktop.
XState Next test case
/*
Run against HEAD of `next`.
The console output will likely make the order of operations clear, it is designed to try and guide you through the behavior.
Two issues:
- The first invoked promise does not trigger a done event, so the timeout catches and that state node re-enters.
- The `assign` for the later state (spawn) is running before an action for the previous state (reset).
One curiousity:
- The onDone for `invocable-promise` doesn't appear to be triggered. Arguably shouldn't for spawned machines.
*/
import { assign, interpret, Machine } from 'xstate';
import { invokeMachine, invokePromise } from 'xstate/invoke';
// Unused, but included for context.
function applicationConfigGen() {
return {
id: 'application',
type: 'parallel',
states: {
'child-component': childComponentConfigGen()
}
};
}
// Unused, but included for context.
function childComponentConfigGen() {
return {
id: 'child-component',
initial: 'load',
states: {
'load': {
invoke: {
src: invokeMachine(Machine(cancellablePromiseConfigGen())),
onDone: [
{ target: 'resolve', cond: (context, event) => { return event.data && event.data.target === 'resolve' } },
{ target: 'reject', cond: (context, event) => { return event.data && event.data.target === 'reject' } }
]
}
},
'resolve': { type: 'final' },
'reject': { type: 'final' }
}
};
}
// Debugging counter function.
var i = 0;
function spawnIdentifier() {
i++;
return `invocable-promise-${i}`;
}
// External closure reference since assign happens too soon and `stop()` is being called on the wrong thing.
var previous;
// Entry point.
function cancellablePromiseConfigGen() {
return {
id: 'cancellable-promise',
initial: 'spawn',
context: {},
states: {
'reset': {
always: [
{
target: 'spawn',
actions: [
(context) => {
console.error(`cancellable-promise.reset: this is occurring *after* cancellable-promise.spawn's assign`);
console.error(`cancellable-promise.reset: context.promise.stop() would stop ${context.promise.name} instead, which is wrong. Attempting to stop invocable-promise-1.`);
// BUG: this would send stop to the wrong thing, so using closure-based caching instead:
// context.promise.stop();
previous.stop();
}
]
}
],
},
'spawn': {
always: [
{
target: 'waiting',
actions: [
// This will spawn an `invocable-promise` machine on init.
assign((context, event, { spawn }) => {
var name = spawnIdentifier();
// FIXME: saved off to the side to stop the correct thing.
previous = context.promise;
console.log(`cancellable-promise.spawn: executing the assign for ${name}`);
return {
promise: spawn.from(Machine(promiseConfigGen()), name)
};
}),
]
}
],
},
'waiting': {
after: {
// This timeout is significantly *longer* than the spawned `invocable-promise` machine's
// resolution. This `after` transition should never be triggered as the `cancellable-promise`
// machine should have already transitioned into its final state.
// It should be in its final state because the spawned, synced, `invocable-promise` machine
// should reach its final state, before it can resolve.
10000: 'reset'
},
on: {
'TIMEOUT': 'timeout'
}
},
'timeout': {
on: {
'ABORT': 'abort',
'RETRY': { target: 'reset' },
'WAIT': 'waiting'
}
},
'abort': { type: 'final', data: { target: 'abort' } },
'resolve': {
entry: [
() => { console.log('cancellable-promise reached final state'); }
],
type: 'final',
data: { target: 'resolve' }
},
'reject': { type: 'final', data: { target: 'reject' } }
},
on: {
'*': [
{
target: 'resolve',
cond: (context, event) => {
console.log(`cancellable-promise received ${event.type}`);
return true;
}
},
]
}
};
}
function promiseConfigGen() {
return {
id: 'invocable-promise',
initial: 'promise',
states: {
'promise': {
invoke: {
src: invokePromise((context, event, meta) => new Promise(function(resolve) {
context, event, meta;
console.log('invocable-promise.promise: executing promise body');
// This timeout is significantly *shorter* than the `waiting` state timeout in
// `cancellable-promise`. This means that this machine should go to its final state
// prior to the timeout in `cancellable-promise` being reached.
// The `cancellable-promise` parent should receive an event, which should trigger
// it *also* reaching its final state.
// This should result in a state machine that stops after this timeout.
setTimeout(resolve, 3000);
})),
onDone: 'resolve',
onError: 'reject'
}
},
'resolve': {
entry: [
(context, event, meta) => {
console.log(`${meta.state._sessionid} reached final state`);
// BUG: Reaching the final state the first time, it is not sending a `done` event to the parent.
if (meta.state._sessionid === 'invocable-promise-1') {
console.error('This should have triggered done.invoke.invocable-promise-1 and exit immediately, but it did not.');
}
}
],
type: 'final'
},
'reject': { type: 'final' }
},
onDone: [
{
target: 'resolve',
cond: () => {
// INTERESTING: this is never reached.
debugger;
return true;
}
}
]
};
}
const applicationMachine = Machine(cancellablePromiseConfigGen());
const applicationInterpreter = interpret(applicationMachine);
applicationInterpreter.onTransition((state) => {
console.log('cancellable-promise state:', state.value);
});
applicationInterpreter.start();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment