Last active
November 14, 2019 18:36
-
-
Save balazsbajorics/8a3c88cc8ea7ec9d1845b66282eb1c9c to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
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
const bundlerMachine = Machine({ | |
id: 'bundlermachine', | |
type: 'parallel', | |
// the initial context. very similar to a redux-like state object. | |
// the context object is immutable, and you must use Actions (side effects) to change its value | |
// the Action to change the context's value is called `assign`. dont ask me why. | |
context: { | |
queuedUpdateFiles: {}, | |
weWantToEmit: false, | |
fileQueuedForProcessing: null, | |
}, | |
// the top level state machine has two sub-machines: worker, and queue. | |
states: { | |
// the worker state machine is responsible for communicating with the worker via Promises | |
worker: { | |
initial: 'uninitialized', | |
states: { | |
// it starts in an unitialized state, the only valid message it can respond to is INITIALIZE | |
uninitialized: { | |
// so what is happening here? `on` is an object that is in the shape of | |
// `{[eventName: string]: TransitionObject} | |
// INITIALIZE is the event we are listening to, and as a | |
// response we transition to a new state called `initializing` | |
// we could add configuration here, fire Actions (side effects) | |
// and optionally we can omit `target`, which means we do not switch state, just fire side effects for example. | |
on: { | |
INITIALIZE: { | |
target: 'initializing', | |
}, | |
}, | |
}, | |
// This state invokes the initializeWorkerPromise promise. (passed in as configuration) | |
// when the promise resolves, the onDone handler will be executed | |
initializing: { | |
invoke: { | |
id: 'initializeBundler', | |
// this here is a string key, we expect the same string to be in the config object. | |
// instead of a string key, this could be a function call that returns a Promise, for faster prototyping | |
src: 'initializeWorkerPromise', | |
// when the promise resolves, the onDone event is fired. | |
onDone: { | |
// the transition object, describing we want to go back to the 'idle' state, | |
// and as a side effect console.log('INITIALIZEd!') | |
target: 'idle', | |
actions: [actions.log(() => 'INITIALIZEd!')], | |
}, | |
}, | |
}, | |
idle: { | |
// when we enter the idle state, fire a POP event, just in case the queue is not empty | |
entry: [send('POP')], | |
on: { | |
// when a PUSH event enters the system, put a POP event to the back of the State Machine's event queue. | |
PUSH: { | |
// the POP event will make our queue state machine to fire a PROCESS_FILE_FROM_QUEUE event | |
actions: [send('POP')], | |
}, | |
// the PROCESS_FILE_FROM_QUEUE event means the queue was not empty, | |
// and now there is a file ready to be processed, let's switch states! | |
PROCESS_FILE_FROM_QUEUE: { | |
target: 'processing', | |
}, | |
}, | |
}, | |
processing: { | |
invoke: { | |
id: 'sendUpdateFileMessage', | |
// let's call the updateFileWorkerPromise, and wait until it resolves | |
src: 'updateFileWorkerPromise', | |
onDone: { | |
// success! the promise resolved, we can go back to idle | |
target: 'idle', | |
// but before we go,... | |
actions: [ | |
assign({ | |
// let's delete the property named fileQueuedForProcessing from context | |
fileQueuedForProcessing: (context, event) => { | |
// instead of this callback function, I could just write `null` here, except typescript complained | |
return null | |
}, | |
}), | |
// and one more logging side effect! | |
actions.log((context, event) => { | |
return 'UPDATE BUNDLE SUCCESSFUL' | |
}), | |
], | |
}, | |
}, | |
}, | |
}, | |
}, | |
// queue is a state machine implementation of Rheese's queueing code | |
queue: { | |
initial: 'empty', | |
states: { | |
// to be honest, I'm not sure we need an empty AND a ready state too, | |
// but at least this way, it is very explicit that you can not POP an empty queue, | |
empty: { | |
on: { | |
PUSH: { | |
target: 'pushing', | |
}, | |
}, | |
}, | |
// this is the non-empty idle state of the queue state machine | |
ready: { | |
on: { | |
PUSH: { | |
target: 'pushing', | |
}, | |
POP: { | |
target: 'popping', | |
}, | |
}, | |
}, | |
pushing: { | |
// entry describes a list of actions (side effects) to be executed when we step into this state | |
// it has a dual, called `exit` | |
entry: [ | |
// assign is like React.setState for the `context` object. here we describe that we want to set | |
// the value of queuedUpdateFiles to include event.payload.fileContent, and | |
// weWantToEmit to be event.payload.weWantToEmit | |
assign({ | |
queuedUpdateFiles: (context, event) => { | |
const updatedQueue = { | |
...context.queuedUpdateFiles, | |
[event.payload.fileName]: event.payload.fileContent, | |
} | |
return updatedQueue | |
}, | |
// btw do you see that there's a lot of `(context, event) => {}` callbacks? it is because | |
// every action, no matter where we are executing receives the same params. | |
// `context` is the latest context of the entire state machine | |
// `event` is the event that the state machine is currently acting on. | |
// notice how we were in the empty/ready state when a PUSH event was fired, | |
// event here is that push event, with its payload. | |
// I was too lazy to type the events up, but xstate is pretty typescript-friendly, so don't worry friends! | |
weWantToEmit: (context, event) => event.payload.weWantToEmit, | |
}), | |
], | |
on: { | |
// the `''` event is a special event, that is fired when we step into this state. since I am | |
// describing a transition with a target as the handler, | |
// it basically means "as soon as we step into the state called `pushing`, move to a new state" | |
'': [ | |
// everywhere else in the system, I just have a single object as the Transition, but here we have an | |
// array of two objects! What's up with that? the answer is `cond`... | |
{ | |
target: 'empty', | |
// `cond` is a Guard. a guard is a function that receives the usual `(context, event)` parameters | |
// and must return true if we allow the state machine to transition to the state described in the object | |
// if it returns false, the evaluator will move on to the next Transition objet in the array | |
cond: (context) => Object.keys(context.queuedUpdateFiles).length === 0, | |
}, | |
{ | |
target: 'ready', | |
}, | |
], | |
}, | |
}, | |
popping: { | |
entry: [ | |
assign((context) => { | |
// I probably should move this to a helper function, because it looks really ugly here inline | |
// this is essentially copy-paste from the BundlerWorker class | |
const filenames = Object.keys(context.queuedUpdateFiles) | |
const queueLength = filenames.length | |
const lastOne = queueLength === 1 | |
// Only emit if this is the last queued file and we actually wanted it to emit | |
const emitWithThisFile = context.weWantToEmit && lastOne | |
const emitNextBuild = lastOne ? false : context.weWantToEmit | |
const filename = filenames[0] | |
const content = context.queuedUpdateFiles[filename] | |
const updatedQueue = { | |
...context.queuedUpdateFiles, | |
} | |
delete updatedQueue[filename] | |
return { | |
...context, | |
queuedUpdateFiles: updatedQueue, | |
fileQueuedForProcessing: { | |
fileName: filename, | |
fileContent: content, | |
shouldEmit: emitWithThisFile, | |
}, | |
weWantToEmit: emitNextBuild, | |
} | |
}), | |
// we fire the PROCESS_FILE_FROM_QUEUE, letting the bundler state machine know it can start working! | |
send('PROCESS_FILE_FROM_QUEUE'), | |
], | |
on: { | |
// same as in pushing, I should remove this copy paste, either by making this object a global const, | |
// or by coming up with a better representation than the empty/ready states. | |
'': [ | |
{ | |
target: 'empty', | |
cond: (context) => Object.keys(context.queuedUpdateFiles).length === 0, | |
}, | |
{ | |
target: 'ready', | |
}, | |
], | |
}, | |
}, | |
}, | |
}, | |
}, | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment