-
Notifications
You must be signed in to change notification settings - Fork 358
Add hot reloading and make it possible to stop Elm apps #1155
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
base: master
Are you sure you want to change the base?
Conversation
`Elm.hot.reload(newScope)` updates the `Elm` namespace and hot reloads all running Elm app instances. The compiler should first check that a hot reload is safe: If anything has changed in the "deep canonicalized" type of `main`, it is unsafe. Then the page should be reloaded instead. It needs support in elm/browser to work. (And in elm/virtual-dom for `main : Html msg` programs.)
These prevented apps from being garbage collected.
Ports are the only effect managers that can change. We need to not only check if the port already exists in the previous code, but also make sure that the correct data can flow through it, and that it goes in the right direction.
|
Thanks for suggesting these code changes. To set expectations:
Finally, please be patient with the core team. They are trying their best with limited resources. |
Co-authored-by: Jeroen Engels <[email protected]>
| var _Platform_worker = F3(function(impl, flagDecoder, debugMetadata) | ||
| { | ||
| return _Platform_initialize( | ||
| flagDecoder, | ||
| args, | ||
| impl.__$init, | ||
| impl.__$update, | ||
| impl.__$subscriptions, | ||
| function() { return function() {} } | ||
| ); | ||
| var init = function(args) | ||
| { | ||
| return _Platform_initialize( | ||
| flagDecoder, | ||
| args, | ||
| null, | ||
| null, | ||
| null, | ||
| function() { return function() {} }, | ||
| impl | ||
| ); | ||
| }; | ||
|
|
||
| /**__DEBUG/ | ||
| init.hotReloadData = { | ||
| __$impl: impl, | ||
| __$platform_effectManagers: _Platform_effectManagers, | ||
| __$scheduler_enqueue: __Scheduler_enqueue | ||
| }; | ||
| //*/ | ||
|
|
||
| return init; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All functions that wrap _Platform_initialize (also in elm/browser) have been changed this way:
- Decrease from
F4toF3and move the last argument to an explicit function (calledinit) inside. That’s equivalent – just another way to express the curriedness. - Attach
.hotReloadDataon that inner function in__DEBUGmode.
The inner function is called init because that’s the init function that ends up at Elm.Main.init for example. So for each Elm entrypoint, there’s .init() to initialize an app as usual, and .init.hotReloadData for use when hot reloading already initialized apps of that entrypoint.
Code that I’ll comment on later removes .hotReloadData, so there’s no risk that people start depending on it.
| __Result_isOk(result) || __Debug_crash(2 /**__DEBUG/, __Json_errorToString(result.a) /**/); | ||
| var managers = {}; | ||
| var initPair = init(result.a); | ||
| var initPair = impl.__$init(result.a); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You’ll see this a lot in the diff: Instead of popping off stuff from from impl (like var init = imp.__$init; init()), we pass around the whole impl object and read from it all the time (impl.__$init()).
This allows us to simply mutate the impl object later (impl.__$init = newInit) to swap in new versions of the functions. That’s the core of how the hot reloading works!
| /**__DEBUG/ | ||
| app.hotReload = function(hotReloadData) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just like with .hotReloadData that I mentioned earlier, code that I’ll comment on later removes this .hotReload method on apps, so there’s no risk that people will depend on it.
In other words, in non-production mode we “smuggle” some more data/functions out to _Platform_export so it can create Elm.hot.reload – that’s the only exposed thing.
| if (manager.__portSetup) | ||
| { | ||
| ports = ports || {}; | ||
| ports[key] = manager.__portSetup(key, sendToApp); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason for these changes is preventing a memory leak, that causes stopped apps never to be garbage collected. See the individual commits of this PR for the full story.
| obj[name] = _Platform_wrapInit(moduleName, exp); | ||
| scope['Elm'].hot.__hotReloadData[moduleName] = exp.hotReloadData; | ||
| delete exp.hotReloadData; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned earlier, this is where we remove .hotReloadData (from Elm.Main.init for example). Instead it is stored at Elm.hot.__hotReloadData["Main"] for example.
(Note that Elm.hot.__hotReloadData is a flat object. For Elm.Blog.Home the hot reload data is stored at Elm.hot.__hotReloadData["Blog.Home"].)
| var app = init(args); | ||
|
|
||
| var hotReload = app.hotReload; | ||
| delete app.hotReload; | ||
| var reloadFunction = function () | ||
| { | ||
| hotReload(scope['Elm'].hot.__hotReloadData[moduleName]); | ||
| }; | ||
| scope['Elm'].hot.__reloadFunctions.push(reloadFunction); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned earlier, this is where .hotReload is removed from app.
Instead we store it in Elm.hot.__reloadFunctions.
Also notice how app.hotReload is called with init.hotReloadData. In essence, this lets a running app get the impl from a new version of the same Elm entrypoint and update itself with it.
Also notice how we read from scope['Elm'].hot all the time. That’s because in _Platform_export__DEBUG we mutate stuff in scope['Elm'].hot when we hot reload.
It’s a bit mind-bend:y, but this allows apps initialized from some earlier version of Elm code have access to the latest stuff from the latest hot reload.
This takes all the learnings from elm-watch and implements hot reloading directly in Elm. I’m really happy with how it turned out, it’s pretty simple and clean. “Implements hot reloading” means that there’s now a
Elm.hot.reload()JavaScript function that tools can use. It’s up to the tool to do file watching and stuff like that.This also makes it possible to shut down an app, which is useful when embedding an Elm app in a web component or React component.
Those two are batched together since they touch on the same parts.
See the new javascript-interface.md file for all the details.
Companion PR:s: