I’ve ran into several situations where LiveView and gen_statem being combined would be ideal. The simplest example would be any kind of calculator project. If you make even a simple calculator in LiveView, you’ll eventually want to think about state to keep track of where you are.
I know LiveView is a process running some kind of specialized gen_server. So it’s behavior and state. Similar situation with gen_statem. The only way I can think to combine them is to kick off and link a gen-statem on a LiveView mount, but that uses two processes per client.
Is there a way to do this with one process? Or would I have to write a gen_server from scratch that emulates both LiveView and gen_statem?
It really depends on exactly which properties of gen_statem you need - for instance, do you need the state machine to move between states autonomously (versus only changing when it receives a message)?
I’d say that should be a default way to go. Moreover - that gen_statem would want to live under a different supervisor and be registered under a name that LiveView process for a connection can uniquely identify (for example some user_id)… The reason is - LiveView process might get restarted because of faulty internet connection of a client… and you don’t want to lose the state of the gen_statem, do you?
The number of processes used is still O(n) where n is the number of active LiveView connections… (how many concurrent LiveView connections you expect to have simultaneously? 1k? 10k? BEAM by default allows upto 1m processes )
And thinking of optimizing the number of processes in such case seems like a premature optimization.
Usually it becomes a concern when the logic might dynamically spawn unbound number of processes for some computation… for example, if you want to implement some sort of a BFS… and even then there are mechanisms to workaround - such as using process pools which would limit the number of processes used per computation.
I think calling this a premature optimization is fair in many cases. Conversely, I do think choosing to manage state as a state machine instead of the very general purpose gen_server isn’t unreasonable in some cases.
Even LiveView’s callback handle_event is similar to gen_statem. The key characteristic being the inability to match on the current state (or mode for another word). I mean, it’s possible to stick it in the socket assigns, but that gets very nested.
Regarding disconnects, it’s an interesting idea for an app to be recoverable. I think the Elixir tutorial covers this with its KV Buckets example. Is it typical in non-trivial LiveView apps to do this? To have something running that the LiveView process kicks off and later reconnects to?
I don’t have a specific use case per se. I’ve simply ran into situations where the gen_statem behavior would be really, really nice. Calculators, certain complex/conditional forms, etc.
Is it typical in non-trivial LiveView apps to do this? To have something running that the LiveView process kicks off and later reconnects to?
I like to think of most of LiveViews as of controllers (except render callback). Trying to keep them thin. Delegate most of the work to functions defined in the “core”. In other words LiveView takes care of the “presentation layer” and shouldn’t be used for “business layer”. The fact that it uses GenServer underneath and is stateful - doesn’t mean I should “abuse” its state.
With “Let it Crash” in mind - you should think of what’s going to happen if the process crashes?
LiveView will take care of reconnection, scroll position, some forms and UI components will get their state back via assigns and whatnot… That’s all about the presentation layer. LiveView
For your calculator project, it would be part of the “business layer” - a module that would provide with public function to start a new calculator, and functions to do operations on it.
And then with LiveView build a UI that would allow users to create and interact with the calculators.
Similarly you could add controller to provide with a json api to the calculator project etc.