Open georgefst opened 1 month ago
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:
binary
or aeson
instead of Read
/Show
(or even foreign-store
?)./tmp
or somewhere rather than the working directory.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.Default
instances. This might be easiest if we serialise to JSON.* 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.
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.
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 useghc-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
.
Sure, there should be some prior art on local storage for reference as well
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.
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
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
'sdebug
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?