Skip to content

Instantly share code, notes, and snippets.

@supermario
Created May 14, 2021 21:44
Show Gist options
  • Save supermario/4c2615806c6c561a16edf5dd7208a759 to your computer and use it in GitHub Desktop.
Save supermario/4c2615806c6c561a16edf5dd7208a759 to your computer and use it in GitHub Desktop.
Elm app unmounting

Elm currently has no concept of "unmount". The topic has been talked about a few times:

However none of the suggested workarounds allow an Elm app's vars to be fully released.

Lamdera context

Lamdera's Evergreen system means backend app lifetimes are not finite as in a browser window.

New code deployments result in new Elm app source being loaded, a new .init run, and data transferring from the old app to the new app.

We need to somehow unmount and clear out the old app after a deploy is done.

Without any JS modifications

Here's what we can do without JS:

  • Use a type alias Model = Maybe (...), and set it to Nothing when we need to unmount our app, this lets us GC the Model

  • Unsubscribe from all subscriptions inside the app:

    subscriptions model =
        if model == Nothing then
          Sub.none
        else
          ...
    
  • Keep track of all the port subscription handlers we've ever set, and remove them:

    var handler = function() { ... }
    app.ports.somePort.subscribe(handler)
    
    // ... later when unmounting, must have retained the handler val
    app.ports.somePort.unsubscribe(handler)

However, the following memory leaks are left which it seems we cannot do anything about:

  • The managers, stepper, and ports vars in the old Elm app
  • The loaded source code of the old Elm app (won't unload if source ref vals are still active AFAICT)

JS modification solution

The only way I've found to actually unmount the app entirely and allow GC to cleanup everything, is to add a die() method to the app that references some of the private vars in the Elm runtime.

This needs to be inside the scope of the _Platform_initialize function. In Lamdera we modify the return here like so:

const die = function() {
  console.log('App dying')
  managers = null
  model = null
  stepper = null
  ports = null
}

return ports ? { ports: ports, die: die } : { die: die };

Now app.die() can be called on an Elm app to force the release of remaining vars.

The final piece is unloading the Elm code, the possibility of which may depend on the method of loading.

If using require on Node.js, it can be done like so:

// at load time:
var someref = require('path/to/App.elm')

// at unload time:
const oldAppModule = require.resolve('path/to/App.elm')
delete require.cache[oldAppModule]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment