I've previously covered the NestJS API envisioned for OctoFarm in https://hackmd.io/@BL3YyzEaQX-wcYkdWAt0ew/SkGTeVgfO. Now I will cover the state management, persistence and consistency of updating OctoPrint state in OctoFarm.
First, I'll cover how state is produced and what is currently inconsistent. After that we'll discuss how to manage and persist state. Finally we discuss recurring state updates on a scheduler with a debuggable backoff mechanism.
The WebSocket connections need to be set-up and validated before any further sub-state can be reduced. For example, if a printer is failing to connect over Serial, querying the bed temperature will result in outdated state. Therefore, the state 'serial-timeout' should hide the printer statistics last known. However, that does not mean we should drop/nullify that sub-state (bed/room/nozzle temperature). This simply means that a state reducer should hide the value or mark it as frozen until serial connection is restored.
This is where a produced state should carry consistent knowledge about how to reduce it (this is called 'cleaning' in API V1, here we call it 'selecting' or 'reducing'). Also the state should come with operators to mold it ('actions').
OctoPrint provides multiple types of transport, one in particular is WebSocket (WS) which can autonomously push updates about the monitored 3D printer back to OctoFarm. However, this WS transport needs to be setup first and once its setup it needs to be consistent. It is known that OctoPrint Global API key results in the Websocket connection being setup but it's completely silent. Therefore, we'd like to analyze on how we can really construct a connection state which is consistent and valid.
This needs API query work to be done on OctoPrint before accessing the WebSocket:
The connecting state comprises multiple sub-states indicating this sequential step of verification. We can then query this connection state and provide visual feedback or errors to the user based on presentation logic. For example, wrong API key could have a light-red colored, connection authentication errors could be dark-red, wrong groups/permissions could be orange, and a slow connection could be yellow, green for everything OK, etc.
Also, we can make an action to reset ourselves to a certain sub-state. Maybe the API key was changed within OP? Maybe the user got groups removed? You should adjust in a consistent manner, but never do more than necessary and keep all adjustments within the state.
We can model the connection state with the previous knowledge. Do note, the following model may contain properties which overlap and some are orthogonal. Therefore a single state is not valid (enum type) and would lead to inconsistencies. Therefore, it's best to model it in explicit states, also for debugging purposes.
We will take this model and apply it next.
Explaining what the redux pattern is, how it helps managing state and what immutability can do for us.
https://redux.js.org/faq/immutable-data#redux-faq-immutable-data
Big disadvantage is that code footprint (often) increases in size and complexity because of it. I will show this to you with the code snippet below.
Immutable data management ultimately makes data handling safer.
The take-away is that doing state changes explicitly and in repeatable fashion can introduce debugging and reproduction help. This is huge. A timeline to scroll through whilst seeing your state tracking back… it's such a cool feature of Redux Devtools (Chromium extension). Such a thing can help us greatly into testing what went wrong in a certain environment. I think it's called 'State Replay'. (Definitely a great tool for VueJS as well!)
Now let's our head out of the clouds, and put our money where our mouth is. How do we do this state mumbo jumbo? I'll give an example.
Example time
Imagine a printer temperature state which is shared across multiple services, service A and B. Lets just assume service A reassigns the printer state because it is convinced it is completely inconsistent. Suddenly, service B is handling an old object reference which is not the same as service A anymore. This state has become invalid.
How do we fix this? We should use 'read-only' state copies to check our state by fetching a frozen copy of the sub-state printer connection. We are reducing our application state to a specific printer connection (reduction) by a selector, like a printer ID. Now when service A wants to update the state, it calls a overwrite/patch action to perform the update on the sub-state. Service B will always query the state at it's own will, but we can also notify it of the change (subscription). We've created a central state producer for decentral state consumers.
Now, let's take a look of what an immutable state can look like. In the following code snippet we are explicitly trying to adjust the corsEnabled
sub-state property after our API Key process has completed correctly:
Well, not a lot.
This implements a redux-like pattern without further dependency, but not more. Nice? Sure. Useful? Eh.
https://github.com/danmt/state-mgmt-sample
This implements NGRX, something which I am not fan of. Also, its probably buggy:
https://github.com/derekkite/ngrx-nest
So, it's up to us to 'do it our way', but hey at least we have comparison in React/Angular/Vue and we have resources to learn from!
This section covers how we will act on synchronizing and recurring state updates with asynchronous state producers like OctoPrint WebSocket.
Discuss the advantage of persisting state from a top-to-bottom or bottom-to-top approach. Differences in storing data in that database first, or memory first.
How to keep state consistent with respect to remote state changes.
Keeping state, tracking state and persisting state.
In this section we cover how agenda, bull, redis and node-cron can help us fix different kinds of problems for recurring/one-shot types of jobs. We first define what jobs we expect and then we allocate or discuss them within the different contexts.
Discuss how to manage scheduled jobs and being able not fire off new ones