diff --git a/javascript-interface.md b/javascript-interface.md
new file mode 100644
index 00000000..91c8d2d6
--- /dev/null
+++ b/javascript-interface.md
@@ -0,0 +1,248 @@
+# JavaScript interface
+
+When you compile Elm to JavaScript using `elm make MyModule.elm --output my-file.js`, how do you actually use that JavaScript file?
+
+## Loading the file
+
+### Browser
+
+```html
+
+```
+
+(Note: The modern `type="module"` attribute or JavaScript `import` syntax won’t work. It has to be a plain script tag like above.)
+
+The script creates a global variable called `Elm` where all the Elm things are:
+
+```js
+var app = Elm.MyModule.init();
+```
+
+If the `Elm` global variable already exists, it’s augmented. This allows you to load multiple Elm compiled JavaScript files on the same page.
+
+### Node.js
+
+`Platform.worker` programs can be used in [Node.js](https://nodejs.org/).
+
+(The other program types can be used in Node.js, too, if you use the [jsdom](https://github.com/jsdom/jsdom) package.)
+
+Node.js has two module systems: The old “CommonJS” system, and the newer “ESM” system.
+
+For CommonJS:
+
+```js
+var { Elm } = require('./my-file.js');
+var app = Elm.MyModule.init();
+```
+
+For ESM:
+
+```js
+import { createRequire } from 'node:module';
+var require = createRequire(import.meta.url);
+var { Elm } = require('./my-file.js');
+var app = Elm.MyModule.init();
+```
+
+Alternative way:
+
+```js
+import fs from 'node:fs'; // or: var fs = require('fs');
+var code = fs.readFileSync('./my-file.js', 'utf-8');
+var f = new Function(code);
+var scope = {};
+f.call(scope);
+var { Elm } = scope;
+var app = Elm.MyModule.init();
+```
+
+## `Elm.MyModule.init()`
+
+If your Elm module is called `MyModule`, you can initialize it like so:
+
+```js
+var app = Elm.MyModule.init({
+ flags: myFlags,
+ node: myDomNode
+});
+```
+
+If the Elm module is `Blog.Home` it’s `Elm.Blog.Home.init()`, and so on.
+
+`init` takes an object with up to two properties:
+
+- `flags`: If your program takes flags (the first parameter of your Elm `init` function), this is where you pass them.
+- `node`: Pass a DOM node to let Elm take over here, if your program requires one.
+
+You can leave out properties that aren’t needed for your program, and the entire object if none of them are needed. Which ones can be used depend on the program type:
+
+| Program | `flags` | `node` |
+| --------------------- | -------- | ------------------------ |
+| `Html` | ignored | required |
+| `Browser.sandbox` | ignored | required |
+| `Browser.element` | optional | required |
+| `Browser.document` | optional | ignored, always `
` |
+| `Browser.application` | optional | ignored, always `` |
+| `Platform.worker` | optional | ignored, no `view` |
+
+The return value is an object with a few properties and functions described below. It’s typically called `app`.
+
+## `app.ports`
+
+If you define ports – _and use them_ – they’ll be available at `app.ports`.
+
+For `myOutgoingPort : String -> Cmd msg`, use `app.ports.myOutgoingPort.subscribe(function(string) { console.log(string) })`. There’s also `unsubscribe` – put your listener function in a variable and pass it to `unsubscribe` to stop listening.
+
+For `myIncomingPort : (String -> msg) -> Sub msg`, use `app.ports.myIncomingPort.send("hello")`.
+
+If you don’t use any ports in your program, `app.ports` does not exist. (It’s _not_ an empty object!) If `app.ports` is unexpectedly missing, double check that your port actually ends up being used – all the way from where you call it, until its return value reaches `main`.
+
+## `app.stop()`
+
+This function stops your Elm app. This is useful for example when embedding your Elm app in a web component. When the web component unmounts, stop the Elm app.
+
+`view`, `update` and `subscriptions` are stopped immediately, but pending `Cmd`s such as HTTP requests aren’t cancelled. When they finish, the messages they produce are silently ignored. At that point, if you no longer keep a reference to `app` it will be garbage collected.
+
+The return value is the root DOM node for the app (or `null` for `Platform.worker`).
+
+The first thing `app.stop()` does is call `app.detachView()` (unless you already did that yourself), so read on about that function below for more details.
+
+## `app.detachView()`
+
+This is for advanced use cases. You probably want to use `app.stop()`.
+
+`app.detachView()` makes Elm release control over the DOM: It removes all event listeners and returns the root DOM node for the app to you. This essentially turns the app into a `Platform.worker` – `update` and `subscriptions` will still run, and ports can be used. (If the app was a `Platform.worker` from the beginning, `app.detachView()` doesn’t do anything and returns `null`.)
+
+As mentioned above for `app.stop()`, there might be pending `Cmd`s such as HTTP requests when you stop your Elm app. If it’s important for you to handle the messages they produce as they finish, use `app.detachView()` rather than `app.stop()`. `app.stop()` silently ignores the messages, while with `app.detachView()` they arrive to your `update` function as usual, and you can handle them somehow. Just remember that your `view` function won’t run anymore!
+
+What `Cmd`s might be pending that you might want to care about?
+
+- HTTP requests: You might have HTTP requests that aren’t finished yet. Note that you can use `Http.cancel` to cancel requests, and set a timeout on requests (using `Http.request`, `Http.riskyRequest` or `Http.task`) to avoid waiting forever. If the responses contains something important to show to the user, you could pass the outputs through a port.
+- `Process.sleep`: You might be waiting for a certain amount of time to pass. If you do this in a loop, consider using `Time.every` instead.
+- `File.Select.file` and `File.Select.files`: The user can have the browser file picker open. This is probably not very likely.
+- `WebGL.Texture.load` and `WebGL.Texture.loadWith`: These download texture files. They don’t seem cancellable. Without a view, you probably don’t care about the texture anymore.
+
+All other commands should finish almost instantly.
+
+It isn’t possible to tell when the app is “done.” You can leave it running for a set time, or until some app-specific condition. With refactoring of Elm’s effect system, it might be possible to say that “no message-producing commands are pending,” but note that it’s still possible to send a value through a port at any time to make the app do work again. It’s also possible to have commands that produce messages, that trigger commands infinitely.
+
+If your app needs to know that it is in this view-less state, use a port to tell it:
+
+```js
+app.detachView();
+app.ports.onDetachedView.send(true);
+```
+
+## `app.stop()` and `app.detachView()` notes
+
+| Program | `app.stop()` | `app.detachView()` | return value |
+| --------------------- | -------------------- | --------------------- | ------------- |
+| `Html` | same as `detachView` | | root DOM node |
+| `Browser.sandbox` | same as `detachView` | | root DOM node |
+| `Browser.element` | | | root DOM node |
+| `Browser.document` | | | `` |
+| `Browser.application` | | also stops navigation | `` |
+| `Platform.worker` | | does nothing | `null` |
+
+- `Html` and `Browser.sandbox` have no effects, so `app.stop()` does not have anything more to do than stopping the view. (Except one thing: The app won’t be garbage collected unless you call `app.stop()`. This is due to hot reloading, and does not apply for `--optimize`.)
+- `Platform.worker` programs have no view, so `app.detachView()` doesn’t do anything, and both `app.stop()` and `app.detachView()` return `null` as there is no DOM node to return.
+- `Html`, `Browser.sandbox` and `Browser.element` programs all require a DOM node when initializing them: `Elm.MyModule.init({ node: myDomNode })`. Elm takes over `myDomNode` and uses it for rendering. Elm might even replace the DOM node with a new one, for example if you passed a `` and then rendered a `` element instead in your Elm view. The return value of `app.detachView()` is the root DOM node at the time, which may or may not be the same one as the `myDomNode` you passed when initializing. You can use the returned DOM node to for example remove it from the page, or pass it to a new Elm app.
+- `Browser.document` and `Browser.application` always use the `` element on the page for rendering. `app.stop()` and `app.detachView()` return that `` element. Unless you’ve done something very unconventional, the return value is the same as `document.body`.
+- `Browser.application` programs react to the user using the back and forward buttons of the browser, by listening to the `popstate` event. This event listener is removed by `app.detachView()`.
+
+Here’s a comparison with trying to stop an Elm app using Elm code vs using `app.stop()` and `app.detachView()`:
+
+```elm
+main =
+ Browser.application
+ -- `init` is run just once, so there’s nothing to stop.
+ { init = init
+
+ -- `subscriptions` can be stopped via a flag in the model.
+ -- `app.stop()` does the equivalent of this, and makes sure `subscriptions` isn’t called again.
+ , subscriptions =
+ \model ->
+ if model.stopped then
+ Sub.none
+
+ else
+ subscriptions model
+
+ -- `update` can be made to ignore messages via a flag in the model.
+ -- `app.stop()` does the equivalent of this, except your `update` won’t even be called.
+ , update =
+ \msg model ->
+ if model.stopped then
+ ( model, Cmd.none )
+
+ else
+ update msg model
+
+ -- `view` can’t be stopped, but it’s possible to render something empty.
+ -- `app.stop()` or `app.detachView()` keeps the last rendered DOM, but removes
+ -- event listeners from it, and makes sure `view` isn’t even called again.
+ , view =
+ \model ->
+ if model.stopped then
+ Html.text ""
+
+ else
+ view model
+
+ -- Since `view` renders an empty text node, there are no links to click.
+ -- `app.stop()` and `app.detachView()` remove the click listeners from all links rendered by Elm.
+ , onUrlRequest = LinkClicked
+
+ -- This can be triggered by all functions that take `Browser.Navigation.Key`, but since
+ -- `update` always returns `Cmd.none` that won’t happen. If the user uses the browser
+ -- back and forward buttons, though, it will trigger.
+ -- `app.stop()` and `app.detachView()` remove the listener for the browser back and forward
+ -- buttons, so it won’t trigger.
+ , onUrlChange = UrlChanged
+ }
+```
+
+## `Elm.hot.reload()`
+
+This is intended for people who develop plugins for build tools.
+
+The plugin could fetch new compiled JavaScript, and use it like so:
+
+```js
+var f = new Function(newCompiledElmCodeAsString);
+var newScope = {};
+f.call(newScope);
+Elm.hot.reload(newScope);
+```
+
+The above updates all modules on `Elm` (such as `Elm.Main` and `Elm.Blog.Home`), so that calling `.init()` on one of them would initialize an Elm app using the latest code rather than the old.
+
+It also updates all running Elm app instances, without losing any state. It does this by injecting updated versions of `view`, `update`, `subscriptions` etc. into them.
+
+(Other properties of `Elm.hot` are private and not supposed to be used.)
+
+If the build tool is ESM based and uses the common `import.meta.hot` API, here’s the basics of it:
+
+```js
+const scope = {};
+
+(function () {
+ // Put the compiled Elm JavaScript code here.
+}).call(scope);
+
+export const Elm = scope.Elm;
+
+if (import.meta.hot) {
+ import.meta.hot.data.Elm ??= Elm;
+
+ import.meta.hot.accept((newModule) => {
+ if (somehowTellIfMainProgramTypeChanged) {
+ import.meta.hot.invalidate('Incompatible Elm type changes.');
+ } else {
+ import.meta.hot.data.Elm.hot.reload(newModule);
+ }
+ });
+}
+```
+
+If the type of `main` has changed (including adding or removing custom type variants or record fields), the page should be reloaded rather than using `Elm.hot.reload()`. The latter could cause runtime errors, since the new functions aren’t compatible with the previous model, for example.
diff --git a/src/Elm/Kernel/Platform.js b/src/Elm/Kernel/Platform.js
index d3cf8db0..5a51a297 100644
--- a/src/Elm/Kernel/Platform.js
+++ b/src/Elm/Kernel/Platform.js
@@ -4,7 +4,7 @@ import Elm.Kernel.Debug exposing (crash)
import Elm.Kernel.Json exposing (run, wrap, unwrap, errorToString)
import Elm.Kernel.List exposing (Cons, Nil)
import Elm.Kernel.Process exposing (sleep)
-import Elm.Kernel.Scheduler exposing (andThen, binding, rawSend, rawSpawn, receive, send, succeed)
+import Elm.Kernel.Scheduler exposing (andThen, binding, enqueue, rawSend, rawSpawn, receive, send, succeed)
import Elm.Kernel.Utils exposing (Tuple0)
import Result exposing (isOk)
@@ -15,16 +15,30 @@ import Result exposing (isOk)
// PROGRAMS
-var _Platform_worker = F4(function(impl, flagDecoder, debugMetadata, args)
+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;
});
@@ -32,26 +46,146 @@ var _Platform_worker = F4(function(impl, flagDecoder, debugMetadata, args)
// INITIALIZE A PROGRAM
-function _Platform_initialize(flagDecoder, args, init, update, subscriptions, stepperBuilder)
+function _Platform_initialize(flagDecoder, args, _init, _update, _subscriptions, stepperBuilder, impl)
{
+ // Old versions of elm/browser do not send the `impl` – instead, they send parts of it separately.
+ // Sending the whole object is required for hot reloading.
+ if (!impl)
+ {
+ impl = {
+ __$init: _init,
+ __$update: _update,
+ __$subscriptions: _subscriptions
+ }
+ }
+
var result = A2(__Json_run, flagDecoder, __Json_wrap(args ? args['flags'] : undefined));
__Result_isOk(result) || __Debug_crash(2 /**__DEBUG/, __Json_errorToString(result.a) /**/);
var managers = {};
- var initPair = init(result.a);
+ var initPair = impl.__$init(result.a);
var model = initPair.a;
- var stepper = stepperBuilder(sendToApp, model);
+ var stepper = stepperBuilder(sendToApp, model, true);
var ports = _Platform_setupEffects(managers, sendToApp);
+ var stopped = false;
function sendToApp(msg, viewMetadata)
{
- var pair = A2(update, msg, model);
+ if (stopped)
+ {
+ return;
+ }
+ var pair = A2(impl.__$update, msg, model);
stepper(model = pair.a, viewMetadata);
- _Platform_enqueueEffects(managers, pair.b, subscriptions(model));
+ _Platform_enqueueEffects(managers, pair.b, impl.__$subscriptions(model));
+ }
+
+ // Initial draw. This used to be done as a side effect of calling `stepperBuilder`.
+ // `stepper.__$shutdown` was introduced at the same time as moving the draw call here,
+ // so we can use that to know if we should draw or not. Drawing here has two benefits:
+ // Firstly, it’s easier to understand. No hidden side effects. The effects are concentrated here.
+ // Secondly, drawing can have side effects, because of custom elements. Their `connectedCallback`s
+ // fire while drawing, and can dispatch (synchronous) events which cause an update. That won’t
+ // work if the draw happened during `stepperBuilder`, since `stepper` won’t be defined here yet.
+ if (stepper.__$shutdown)
+ {
+ stepper(model, true);
}
- _Platform_enqueueEffects(managers, initPair.b, subscriptions(model));
+ // Apply `Cmd`s from `init`, and run `subscriptions` for the first time.
+ _Platform_enqueueEffects(managers, initPair.b, impl.__$subscriptions(model));
+
+ var detachedDomNode = undefined;
+
+ var detachView = function()
+ {
+ // Make a second call to `app.detachView` do nothing.
+ if (detachedDomNode !== undefined)
+ {
+ return detachedDomNode;
+ }
+
+ // Draw synchronously one last time in case we’re waiting for an animation frame.
+ stepper(model, true);
+
+ var stepperShutdown = stepper.__$shutdown;
+
+ // Prevent new draw calls from being made.
+ stepper = function() {};
+
+ // Remove event listeners from the DOM.
+ // This is only available in newer versions of elm/browser.
+ // Also note that `_Platform_worker` does not provide it (since there’s no `view`).
+ detachedDomNode = stepperShutdown ? stepperShutdown() : null;
+
+ // Return the final DOM node produced by Elm (if any).
+ // This is necessarily not the same DOM node as the app was mounted on,
+ // since Elm might replace it, for example if the element type changes.
+ return detachedDomNode;
+ };
+
+ var stop = function()
+ {
+ // Make a second call to `app.stop` do nothing.
+ if (stopped)
+ {
+ return detachedDomNode;
+ }
- return ports ? { ports: ports } : {};
+ // Stop view. This can trigger synchronous updates, which is why we haven’t stopped updates yet.
+ detachView();
+
+ // Stop update. (This makes us ignore messages.)
+ stopped = true;
+
+ // Stop subscriptions.
+ _Platform_enqueueEffects(managers, _Platform_batch(_List_Nil), _Platform_batch(_List_Nil));
+
+ // Same return value as `app.detachView`.
+ return detachedDomNode;
+ };
+
+ var app = {
+ detachView: detachView,
+ stop: stop
+ };
+
+ if (ports)
+ {
+ app.ports = ports;
+ }
+
+ /**__DEBUG/
+ app.hotReload = function(hotReloadData)
+ {
+ // Remove old subscriptions.
+ _Platform_enqueueEffects(managers, _Platform_batch(_List_Nil), _Platform_batch(_List_Nil));
+
+ // This function depends on local state, so we need to update it to the implementation from the new code.
+ __Scheduler_enqueue = hotReloadData.__$scheduler_enqueue;
+
+ // Setup any new effect managers.
+ var newPorts = _Platform_hotReloadEffects(managers, ports, hotReloadData.__$platform_effectManagers, sendToApp);
+ if (!ports && newPorts)
+ {
+ app.ports = newPorts;
+ }
+ ports = newPorts;
+
+ // Replace view, update and subscriptions with implementations from the new code.
+ for (var key in hotReloadData.__$impl)
+ {
+ impl[key] = hotReloadData.__$impl[key];
+ }
+
+ // Draw synchronously with the new view function.
+ stepper(model, true);
+
+ // Set up new subscriptions.
+ _Platform_enqueueEffects(managers, _Platform_batch(_List_Nil), impl.__$subscriptions(model));
+ };
+ //*/
+
+ return app;
}
@@ -91,10 +225,82 @@ function _Platform_setupEffects(managers, sendToApp)
if (manager.__portSetup)
{
ports = ports || {};
- ports[key] = manager.__portSetup(key, sendToApp);
+ var data = manager.__portSetup(key, sendToApp);
+ ports[key] = data.__port;
+ managers[key] = _Platform_instantiateManager(
+ data.__init,
+ data.__onEffects,
+ null,
+ manager.__cmdMap,
+ manager.__subMap,
+ sendToApp
+ );
}
+ else
+ {
+ managers[key] = _Platform_instantiateManager(
+ manager.__init,
+ manager.__onEffects,
+ manager.__onSelfMsg,
+ manager.__cmdMap,
+ manager.__subMap,
+ sendToApp
+ );
+ }
+ }
+
+ return ports;
+}
+
- managers[key] = _Platform_instantiateManager(manager, sendToApp);
+function _Platform_hotReloadEffects(managers, ports, newEffectManagers, sendToApp)
+{
+ for (var key in newEffectManagers)
+ {
+ var manager = newEffectManagers[key];
+ var existing = _Platform_effectManagers[key];
+
+ if (manager.__portSetup)
+ {
+ // If a port already exists and is still outgoing or still incoming:
+ if (existing && (manager.__cmdMap && existing.__cmdMap || manager.__subMap && existing.__subMap))
+ {
+ // Update its converter in case it has been changed to let different data through.
+ existing.__converter = manager.__converter;
+ }
+ else
+ {
+ // Otherwise instantiate a new port. JavaScript already subscribed to the old port
+ // won’t get any more data there (since the new Elm code never sends any).
+ // JavaScript trying to send data through the old port won’t achieve anything,
+ // since the new Elm code doesn’t have it in its subscriptions.
+ _Platform_effectManagers[key] = manager;
+ ports = ports || {};
+ var data = manager.__portSetup(key, sendToApp);
+ ports[key] = data.__port;
+ managers[key] = _Platform_instantiateManager(
+ data.__init,
+ data.__onEffects,
+ null,
+ manager.__cmdMap,
+ manager.__subMap,
+ sendToApp
+ );
+ }
+ }
+ else if (!existing)
+ {
+ // Instantiate new, non-port effect managers.
+ _Platform_effectManagers[key] = manager;
+ managers[key] = _Platform_instantiateManager(
+ manager.__init,
+ manager.__onEffects,
+ manager.__onSelfMsg,
+ manager.__cmdMap,
+ manager.__subMap,
+ sendToApp
+ );
+ }
}
return ports;
@@ -113,18 +319,13 @@ function _Platform_createManager(init, onEffects, onSelfMsg, cmdMap, subMap)
}
-function _Platform_instantiateManager(info, sendToApp)
+function _Platform_instantiateManager(init, onEffects, onSelfMsg, cmdMap, subMap, sendToApp)
{
var router = {
__sendToApp: sendToApp,
__selfProcess: undefined
};
- var onEffects = info.__onEffects;
- var onSelfMsg = info.__onSelfMsg;
- var cmdMap = info.__cmdMap;
- var subMap = info.__subMap;
-
function loop(state)
{
return A2(__Scheduler_andThen, loop, __Scheduler_receive(function(msg)
@@ -142,7 +343,7 @@ function _Platform_instantiateManager(info, sendToApp)
}));
}
- return router.__selfProcess = __Scheduler_rawSpawn(A2(__Scheduler_andThen, loop, info.__init));
+ return router.__selfProcess = __Scheduler_rawSpawn(A2(__Scheduler_andThen, loop, init));
}
@@ -369,8 +570,7 @@ function _Platform_setupOutgoingPort(name)
var init = __Process_sleep(0);
- _Platform_effectManagers[name].__init = init;
- _Platform_effectManagers[name].__onEffects = F3(function(router, cmdList, state)
+ var onEffects = F3(function(router, cmdList, state)
{
for ( ; cmdList.b; cmdList = cmdList.b) // WHILE_CONS
{
@@ -405,8 +605,12 @@ function _Platform_setupOutgoingPort(name)
}
return {
- subscribe: subscribe,
- unsubscribe: unsubscribe
+ __init: init,
+ __onEffects: onEffects,
+ __port: {
+ subscribe: subscribe,
+ unsubscribe: unsubscribe
+ }
};
}
@@ -445,8 +649,7 @@ function _Platform_setupIncomingPort(name, sendToApp)
var init = __Scheduler_succeed(null);
- _Platform_effectManagers[name].__init = init;
- _Platform_effectManagers[name].__onEffects = F3(function(router, subList, state)
+ var onEffects = F3(function(router, subList, state)
{
subs = subList;
return init;
@@ -467,7 +670,11 @@ function _Platform_setupIncomingPort(name, sendToApp)
}
}
- return { send: send };
+ return {
+ __init: init,
+ __onEffects: onEffects,
+ __port: { send: send }
+ };
}
@@ -502,9 +709,45 @@ function _Platform_mergeExportsProd(obj, exports)
function _Platform_export__DEBUG(exports)
{
- scope['Elm']
- ? _Platform_mergeExportsDebug('Elm', scope['Elm'], exports)
- : scope['Elm'] = exports;
+ if (!('Elm' in scope))
+ {
+ // Use `defineProperty` to make `.hot` non-enumerable, so that a for-in loop on `scope['Elm']`
+ // only includes Elm modules. We use that below, and end user code might depend on it too.
+ scope['Elm'] = Object.defineProperty({}, 'hot', {
+ value: {
+ // Usage example:
+ // const f = new Function(newCompiledElmCodeAsString);
+ // const newScope = {};
+ // f.call(newScope);
+ // Elm.hot.reload(newScope);
+ reload: function(newScope)
+ {
+ // Update Elm modules.
+ _Platform_mergeExportsHotReload(scope['Elm'], newScope['Elm']);
+
+ // Update hot reload data.
+ for (var key in newScope['Elm'].hot.__hotReloadData)
+ {
+ scope['Elm'].hot.__hotReloadData[key] = newScope['Elm'].hot.__hotReloadData[key];
+ }
+
+ // Make the _new_ code push to the _old_ list of reload functions,
+ // and read hot reload data from the _old_ dict (which is always merged with the latest above).
+ var reloadFunctions = newScope['Elm'].hot.__reloadFunctions = scope['Elm'].hot.__reloadFunctions;
+ newScope['Elm'].hot.__hotReloadData = scope['Elm'].hot.__hotReloadData;
+
+ // Reload all running Elm app instances.
+ for (var i = 0; i < reloadFunctions.length; i++)
+ {
+ reloadFunctions[i]();
+ }
+ },
+ __reloadFunctions: [],
+ __hotReloadData: {}
+ }
+ });
+ }
+ _Platform_mergeExportsDebug('Elm', scope['Elm'], exports);
}
@@ -512,10 +755,64 @@ function _Platform_mergeExportsDebug(moduleName, obj, exports)
{
for (var name in exports)
{
- (name in obj)
- ? (name == 'init')
- ? __Debug_crash(6, moduleName)
- : _Platform_mergeExportsDebug(moduleName + '.' + name, obj[name], exports[name])
+ var exp = exports[name];
+ if (name == 'init')
+ {
+ if (name in obj)
+ {
+ __Debug_crash(6, moduleName)
+ }
+ else
+ {
+ obj[name] = _Platform_wrapInit(moduleName, exp);
+ scope['Elm'].hot.__hotReloadData[moduleName] = exp.hotReloadData;
+ delete exp.hotReloadData;
+ }
+ }
+ else
+ {
+ _Platform_mergeExportsDebug(moduleName + '.' + name, obj[name] || (obj[name] = {}), exp);
+ }
+ }
+}
+
+
+function _Platform_mergeExportsHotReload(obj, exports)
+{
+ for (var name in exports)
+ {
+ (name in obj && name != 'init')
+ ? _Platform_mergeExportsHotReload(obj[name], exports[name])
: (obj[name] = exports[name]);
}
}
+
+
+function _Platform_wrapInit(moduleName, init)
+{
+ return function(args)
+ {
+ 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);
+
+ var stop = app.stop;
+ app.stop = function()
+ {
+ var index = scope['Elm'].hot.__reloadFunctions.indexOf(reloadFunction);
+ if (index !== -1)
+ {
+ scope['Elm'].hot.__reloadFunctions.splice(index, 1);
+ }
+ return stop();
+ };
+
+ return app;
+ };
+}
diff --git a/src/Elm/Kernel/Scheduler.js b/src/Elm/Kernel/Scheduler.js
index 37a68be8..b945a514 100644
--- a/src/Elm/Kernel/Scheduler.js
+++ b/src/Elm/Kernel/Scheduler.js
@@ -106,6 +106,22 @@ function _Scheduler_kill(proc)
if (task.$ === __1_BINDING && task.__kill)
{
task.__kill();
+ // Here’s how `task.__kill` comes to existence:
+ //
+ // _Scheduler_binding(function (callback) {
+ // // Do stuff and use callback.
+ // // The below function is assigned to `task.__kill` by `_Scheduler_step`.
+ // return function() {
+ // // Do cleanup. Doesn’t use callback, but technically has it in scope.
+ // }
+ // });
+ //
+ // Since `task.__kill` has that `callback` in scope, which eventually references
+ // `sendToApp`, which has access to `model` etc., it’s important to remove it
+ // after using it. Otherwise it can prevent a stopped app from being garbage collected.
+ // `null` is the default value for `__kill` as seen in `_Scheduler_binding`.
+ // It also doesn’t make sense to kill the same task twice anyway.
+ task.__kill = null;
}
proc.__root = null;