Idea: persist liveview state across reconnections

Liveview is nice with one small problem: With mobile, or on shaky network, there will be frequent dropped connections. Yes, Liveview reconnects from the client side automatically, but the state at the server side process is lost. Here is an idea:

  • trap exit in liveview, and when terminate/2 is called, persist the full content of assigns somewhere. (database, flat file, etc), key off an opaque and unique id shared with the client side.
  • at mount, check to see if this unique id can be found in the persistent storage. If so, load the full content into assigns.

There will be some small wrinkles like versioning the saved data, periodic clean up the storage, etc. but it seems like everything should be doable. Any comments?

I’ve done this, but had to separate the user input vs server calculated portions.

For instance, settings are pulled from ETS based on a guid stored in a cookie.

But if the user was inputting something, form recovery must be used as if they disconnect, we don’t want to overwrite what they were doing with the server state. The full state is reconstructed with the form recovery and some expensive data stored in the cache.

So yes, it can be done, just have to be careful in merging the user state with the server state.

1 Like

Fly did an article that uses client-side session storage for this purpose.

Otherwise, my feeling is that whenever possible, state should always be recreateable via the db and more notably the URL—many neglect the latter. I’m even talking things like tabs that may already have the data loaded but hidden. Just giver ‘er a good ol’ window.history.pushState. But I will now step off my soapbox :slight_smile:

3 Likes

Because there might be multiple servers behind one domain and you don’t know which one of those servers WebSocket connection connects that’s why client side state is used. If you have issues with lost state you are not properly storing important state to the client and using that state on reconnect to restore your views. Important state meaning open panels, select element values that are not stored anywhere expect client etc., state that is used to get you to the same view state you where before disconnect but excluding things you can get from your databases. You could example store those open panels to the URL’s query string. You can also use LiveSocket’s params field to send data from client to server on reconnect.

4 Likes

Come to think of it, saving LV state at terminate/2 time is probably not a good idea. The old LV process may terminate after the new LV process got spurn up.

For smallish state, the client side is the best place for storage, as @wanton7 suggested. I still don’t have a solution if the state are big or complex.

I think you can send quite big state through LiveSocket’s params field on reconnect.

What’s an example of the larger state you’re thinking of?

The problem is not so much on how to restore; but on when and how to save. If I need to save a complex data structure to the client side each time something change, then either I lose performance or lose data encapsulation.

@sodapopcan , a big state can be derived from lengthy user interactions such as a game, or result from expensive server side action, like like AI generated data.

Ah ya, I thought “game” after I asked. The AI example I can’t quite imagine—not saying I think you’re wrong it’s just my shortcoming of not being able to imagine it, lol.

For the game scenario, I still haven’t made one myself, but I always figured I’d have a second process dedicated to game to state and the LiveView would just act as the connection state. I know that’s how multiplay games but it could work for single player as well, even though it means every player has at least two processes.

EDIT: This is a semi-question, I didn’t mean to sound like I know what I’m talking about :sweat_smile:

1 Like

For game maybe you could create something similar to Microsoft Orleans Virtual actors with processes? I mean like start a GenServer for the game logic that keeps the state and then just pass reference id to that game process to the client and maybe put some info into database that user has access to that game into database or maybe encode it’s id into a JWT token? That game state could then live even in another server. So if you connect to server1 you could just message game state running on server2 where it was originally started. Then that game state could have timeout that makes it die after certain time passes after no keep alive messages. In this case you should only keep minimal game state information in LiveView’s process. Benefit from this is that you could even have multiple LiveViews connecting to same game.

For AI I think that highly depends on what kind of AI data how much. If it’s images you probably store it somewhere anyway and just provide a link.

1 Like

You can then also use Phoenix.PubSub to send messages when game state changes from that GenServer to all PubSub.subscribe:d LiveView’s. I’ve used something similar in my hobby project where I created this kind paper & pen RPG helper when COVID-19 pandemic was going on. One GenServer was the game session and it handled game ssion logic and it sent state change updates to all LiveView connections subscribed to it’s Phoenix.PubSub events. Worked perfectly.

1 Like

So, I need to have something else (browser, ETS, GenServer, database, external processes. …) to hold the state if I don’t want to lose it.

LV upgrades the state-less HTTP to a wonderful stateful application, with the caveat that it is a unreliable storage and best used as a cache for more reliable storage. :frowning: If I use it as cache, I then need deal with nasty cache consistency problems.

So, I need to have something else (browser, ETS, GenServer, database, external processes. …) to hold the state if I don’t want to lose it.

If you just use it for normal web stuff then you just have to use client to store the important data, that’s pretty much the LiveView programming model. But since you clearly have unnormal use case you want to use it for or maybe you are just mapping what it’s good for? Not every tech is good for everything so maybe you should use something else? But then other tech might have their own issues and you should choose tech that best fits what you are building.

If I use it as cache, I then need deal with nasty cache consistency problems.

Then in your view LiveView connection process is a cache even if you load data from anywhere like from a database, making it a cache pretty much all the time as you have copy of data in lot of cases.

You can also use temporary_assigns and having GenServer that has game logic and keeps game state is very easy to keep in sync as it’s always the source of truth and getting data from it is very fast. If you would create a multiplayer game you would have to use something like that anyway.

If I understood the task properly, you might roll your own event log implementation for each liveview on the client side, which is to be cleaned upon any action. It would contain liveview’s deltas, or like.

If there is a non-empty “event log” for it when the liveview gets loaded, you apply the deltas from it.

1 Like

That sounds complicated, I don’t trust my javascript skill not to mess up. Also, it feels like not a best practice from security point of view.

Here is another idea in a similar vein. Spin up a “shadow” GenServer for each LV process with unique session ID. Wrap assign/2 in my LV process, sending a copy of the new assigns into the shadow server. At mount time, check a Registry to see if a shadow by the same session ID is already there. If so, load all data from there. If not, spin up a fresh one. The shadow server can kill itself after certain time of inactivity.

I’ll burn some resources to make a shadow for each LV process, but it should be straight forward to implement. Any thought?

1 Like

Yes, this sounds better.

I’d suggest you look at the live_state library. It would allow you to offload the state management client-side using a dedicated Channel. It’s fairly trivial to sync a Channel process to the LiveView process to maintain consistency in the UI state using plain old assigns + render, without needing to dip into Hooks. The only hook needed would be to store the LiveState state in localStorage (or what have you) and to handle the reconnect.

I’ve been playing over the weekend with a small tweak to the LiveState JS code that would allow it to accept an arbitrary Socket (i.e. even the primary LiveSocket) and multiplex LiveState instances over a single Socket (library currently spawns its own socket per topic/Channel).

This is one of the use-cases I think live_state fits perfectly.

Do you mean at reconnect, live_state will load full state from the client side? I thought live_state is only unidirectional in term of state change propagation. If it does load state from the client side at reconnect, how would I prevent a hacked client side corrupting my server side state?

My “shadow” GenServer idea works, however, there is a new twist: Now that my server side state is persisted, my client side state is not; the user could reload the tab and all in-memory client side state is cleared. So the restored server side state might not match with the client side state, and I have to “downgrade” the server state to be client side state agnostic.

I think the article mentioned in this would help sync client and server states. fly article sounds promising.

Are there any obvious issues for which you might be avoiding this appraoch?