Created
August 27, 2020 09:07
-
-
Save nathanhammond/f3f6639c3f60e03c787aeeda74389206 to your computer and use it in GitHub Desktop.
XState Next test case
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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