Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proper hot code reloading #749

Open
georgefst opened this issue Oct 22, 2024 · 6 comments
Open

Proper hot code reloading #749

georgefst opened this issue Oct 22, 2024 · 6 comments

Comments

@georgefst
Copy link
Contributor

The sample app README mentions "hot reload". Perhaps these terms aren't all that standardised but my experience from the JavaScript ecosystem is that what we have in the examples, via jsaddle-warp's debug function, where the browser auto-refreshes but state is reset, is called "live" reloading. Whereas "hot" reloading tends to mean actually swapping out code without restarting the program, as associated with languages like Lisp, Erlang and Smalltalk.

As far as I can tell, Miso doesn't support this?

@georgefst
Copy link
Contributor Author

georgefst commented Oct 22, 2024

Of course, many solutions to this problem are pretty hacky and unsound. I don't think any of the JS frameworks or bundlers implementing hot reloading (often referred to as "HMR" - hot module reloading, presumably since modules are the smallest unit replaced) have a great story around what happens when interfaces change in incompatible ways.

But Miso is potentially in a great position to handle this in a principled way, since everything is immutable and the state is a first class entity. It even has a type, so we can detect when changes are incompatible. All we need to do is serialise the state on each update (or on exit, if we can reliably intercept that) and attempt to de-serialise upon reloading*. Come to think of it, this still isn't really true hot reloading, since the code is fully replaced, but it's much simpler and the end result is largely the same.

A hacky working prototype, probably with race conditions, is as simple as:

startAppWithSavedState :: (Eq model, Read model, Show model) => Miso.App model action -> JSM ()
startAppWithSavedState app = do
    loadedInitialState <- liftIO $ doesFileExist filePath >>= \case
        False -> pure Nothing
        True -> readMaybe <$> readFile filePath
    startApp
        app
            { model = fromMaybe app.model loadedInitialState
            , update = \case
                Nothing -> pure -- no-op
                Just a -> \m -> do
                    m' <- first Just $ app.update a m
                    m' <# do
                        liftIO $ writeFile filePath $ show m'
                        pure Nothing
            , subs = mapSub Just <$> app.subs
            , view = fmap Just . app.view
            , initialAction = Just app.initialAction
            }
  where
    filePath = "miso-state"

Various potential improvements:

  • Use binary or aeson instead of Read/Show (or even foreign-store?).
  • Write to a path in /tmp or somewhere rather than the working directory.
  • Strip out the extra logic when compiling to JS/WASM.
  • Maybe modify the actual definition of startApp instead of wrapping it. This would avoid the mess of using Maybe action as the actual action type just to get a no-op action to accompany writing the output.
  • Somehow detect when the state type has changed in a way that can be trivially adapted, such as the removal of a constructor which is unused by the current state, or even the addition of fields with Default instances. This might be easiest if we serialise to JSON.
  • If writing the state on every single update is slow, then just do it when the page is about to be closed ("beforeunload") or maybe navigated away from ("visibilitychange").
  • Provide some mechanism for the user/developer to manually reset the app to the initial state.

* I'm hardly the first person to notice this. This Elm article talking about how much easier the whole problem is in a functional setting is over a decade old. Unfortunately, it's also presumably out-of-date since it mentions FRP. I don't know what support for hot reloading Elm has today.

@dmjio
Copy link
Owner

dmjio commented Oct 22, 2024

Hi,

The nomenclature used around "hot reloading" vs. "live reloading" in the docs might need improvement. According to your description miso has live reloading via jsaddle yes. The code is altered, browser refreshed, and the state gets reset every time.

I like your idea of persisting the state to disk on all changes for jsaddle users. I typically don't use jsaddle, so I haven't experimented too much with this approach. With the JS-backend you can save the state of the miso application to local storage on every event. Since jsaddle for development keeps the state on the server your example above would be the equivalent.

Something like you're describing should exist, like a "Phoenix Live view", etc. would be nice. I'd prefer to dump the state as JSON for it to be readable, but in theory people can do whatever they want.

You could try using inotify to detect file changes and then use ghc-hotswap to recompile / reload modules incrementally. This way the jsaddle websocket connection could be preserved as the code evolves. Instead of a browser refresh there might be a way to notify the frontend to perform a redraw with the new state read from disk. I'd have to check if the jsaddle protocol is extensible.

That's how I'd try to go about doing it.

@georgefst
Copy link
Contributor Author

With the JS-backend you can save the state of the miso application to local storage on every event. Since jsaddle for development keeps the state on the server your example above would be the equivalent.

That's a great point. Would you accept a PR which added a cleaned-up version of startAppWithSavedState based around the local storage API?

You could try using inotify to detect file changes and then use ghc-hotswap to recompile / reload modules incrementally. This way the jsaddle websocket connection could be preserved as the code evolves. Instead of a browser refresh there might be a way to notify the frontend to perform a redraw with the new state read from disk. I'd have to check if the jsaddle protocol is extensible.

Something based around genuine hot-swapping would be amazing to have. But I suspect it has its own caveats (e.g. I don't know how it handles data of a type whose definition has changed), and I suspect it's a lot more work anyway (the library repo is archived, for a start, which isn't a great sign). Given that I already have a feedback cycle well under a second with the example code above, I wouldn't personally be motivated to work on integrating ghc-hotswap.

@dmjio
Copy link
Owner

dmjio commented Oct 22, 2024

Sure, there should be some prior art on local storage for reference as well

@georgefst
Copy link
Contributor Author

jsaddle for development keeps the state on the server

I've realised I'm not actually sure what you mean by this. AFAICT using local storage via JSaddle does the same thing in both environments.

@dmjio
Copy link
Owner

dmjio commented Nov 3, 2024

My understanding is that w/ jsaddle the update function gets executed on the server, the client has an interpreter that modifies the DOM via a websocket protocol. Yes localStorage API stays the same. You could serialize the model to disk on the server, or to localStorage w/ jsaddle. It'd probably be better to do it on the server to avoid round trips.

I thought you were going to mimic the localStorage API on the server and save the serialized model to disk on each call to update

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants