Problem
If you:
- Subscribe to
portA in subscriptions
- Call
portB in update at the same time
- And
portB sends data to portA
… will portA get that data? Will it be subscribed in time?
From an Elm programmer’s perspective it sure looks like the port will be subscribed and should get the data, if you read your subscriptions function (see the SSCCE at the bottom).
The real answer is: It depends. And it depends on what order some generated JavaScript happens to end up in!
The really confusing thing for an Elm programmer is that if you try to debug this with Debug.log in the Elm code and console.log in the JavaScript code (for the ports), it looks like things should be happening. You can see that app.ports.portA.send() is indeed called, but no message is ever seen on the Elm side, even though a Debug.log in subscriptions clearly shows that we do indeed call portA. And the Debug.log happens before the console.log so the order looks correct, too. (Then in reality, things can happen in the opposite order, which explains why nothing happens.)
Use case
I stumbled upon this at work. Our app has multiple pages. Each page has its own Model, Msg, init, update, subscriptions and view, and at the top level we delegate to the current page.
On one page I wanted to get something from local storage when the page initializes. So in init I call a port that reads from local storage, and replies on another port. In subscriptions I subscribe to that other port. Unfortunately, I never got a reply there. It turned out to be because I used the same local storage ports on other pages, and that affected the compiled code order.
Why it happens
This is how Elm calls update and subscriptions:
var pair = A2(update, msg, model);
stepper(model = pair.a, viewMetadata);
_Platform_enqueueEffects(managers, pair.b, subscriptions(model));
It gives the Cmds and Subs to _Platform_enqueueEffects, which passes them on to _Platform_dispatchEffects, which looks like this:
function _Platform_dispatchEffects(managers, cmdBag, subBag)
{
var effectsDict = {};
_Platform_gatherEffects(true, cmdBag, effectsDict, null);
_Platform_gatherEffects(false, subBag, effectsDict, null);
for (var home in managers)
{
__Scheduler_rawSend(managers[home], {
$: 'fx',
a: effectsDict[home] || { __cmds: __List_Nil, __subs: __List_Nil }
});
}
}
It creates one single effectsDict and calls _Platform_gatherEffects twice – once for the Cmds and once for the Subs. _Platform_gatherEffects assigns properties on effectsDict. In this case the properties are the names of the ports.
Then there’s a loop: for (var home in managers). And inside the loop we read from effectsDict[home]. In this case, we have two ports. The order they are executed in comes down to the iteration order of managers. managers is created in _Platform_setupEffects:
function _Platform_setupEffects(managers, sendToApp)
{
var ports;
// setup all necessary effect managers
for (var key in _Platform_effectManagers)
{
var manager = _Platform_effectManagers[key];
if (manager.__portSetup)
{
ports = ports || {};
ports[key] = manager.__portSetup(key, sendToApp);
}
managers[key] = _Platform_instantiateManager(manager, sendToApp);
}
return ports;
}
Here there’s another loop: for (var key in _Platform_effectManagers). We assign to managers[key] in the loop. So the iteration order of managers comes from the iteration order _Platform_effectManagers.
In the case of ports, the _Platform_outgoingPort and _Platform_incomingPort functions assign _Platform_effectManagers[name] (where name is the port name):
function _Platform_outgoingPort(name, converter)
{
_Platform_checkPortName(name);
_Platform_effectManagers[name] = {
__cmdMap: _Platform_outgoingPortMap,
__converter: converter,
__portSetup: _Platform_setupOutgoingPort
};
return _Platform_leaf(name);
}
function _Platform_incomingPort(name, converter)
{
_Platform_checkPortName(name);
_Platform_effectManagers[name] = {
__subMap: _Platform_incomingPortMap,
__converter: converter,
__portSetup: _Platform_setupIncomingPort
};
return _Platform_leaf(name);
}
Those functions are called by generated code which can look like so (taken from the below SSCCE):
var $author$project$PortSubCmdDemoMinimal$getFromLocalStorage = _Platform_outgoingPort(...);
var $author$project$PortSubCmdDemoMinimal$gotTextFromLocalStorage = _Platform_incomingPort(...);
Those lines look like they simply define something at first glance, but remember that they also have the side effect of mutating _Platform_effectManagers. And iteration order of JavaScript objects is based on the insertion order.
Generated definitions seem to be ordered topologically or something. The order is not guaranteed. So depending on where you call those ports, the order can shift.
This means that if you use a port in a new place, you can accidentally break a usage of that port in a completely different part of the app.
Solution?
Process subscriptions before commands, so that the subscriptions are ready for the commands producing data.
SSCCE
port module PortSubCmdDemoMinimal exposing (main)
import Browser
import Html exposing (Html)
import Html.Events
port getFromLocalStorage : { debug : String } -> Cmd msg
port gotTextFromLocalStorage : (String -> msg) -> Sub msg
type alias Model =
{ page : Page
}
type Page
= Home
| Contact String
init : () -> ( Model, Cmd Msg )
init () =
( { page = Home }
-- Flip to the `getFromLocalStorage` line here to trigger the bug.
, Cmd.none
-- , getFromLocalStorage { debug = "via init" }
-- In this case, calling `getFromLocalStorage` here is useless.
-- But imagine adding another `getFromLocalStorage` call in a bigger app
-- and suddenly the original use breaks. Oops!
)
type Msg
= PressedGoToHomePage
| PressedGoToContactPage
| GotLocalStorage String
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case Debug.log "msg" msg of
PressedGoToHomePage ->
( { model | page = Home }
, Cmd.none
)
PressedGoToContactPage ->
-- This text is supposed to be immediately replaced via the two ports.
( { model | page = Contact "❌ Oops! We never got anything from local storage :(" }
, getFromLocalStorage { debug = "via PressedGoToContactPage" }
)
-- Imagine there being a `ContactPage` model with `ContactPage.Msg` that has
-- `GotLocalStorage`. Then the message handling wouldn’t look as silly :)
-- I wanted to keep things simple for the demo, though.
GotLocalStorage text ->
case model.page of
Home ->
( model, Cmd.none )
Contact _ ->
( { model | page = Contact text }, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions model =
case model.page of
Home ->
Sub.none
Contact _ ->
-- Imagine this being `ContactPage.subscriptions |> Sub.map ContactPageMsg`.
gotTextFromLocalStorage GotLocalStorage
view : Model -> Html Msg
view model =
case model.page of
Home ->
Html.div []
[ Html.h1 [] [ Html.text "Home page" ]
, Html.button [ Html.Events.onClick PressedGoToContactPage ]
[ Html.text "Go to contact page" ]
]
Contact text ->
Html.div []
[ Html.h1 [] [ Html.text "Contact page" ]
, Html.button [ Html.Events.onClick PressedGoToHomePage ]
[ Html.text "Go to home page" ]
, Html.p [] [ Html.text ("Text from local storage: " ++ text) ]
]
main : Program () Model Msg
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PortSubCmdDemoMinimal</title>
</head>
<body>
<script src="elm.js" id="node"></script>
<script>
const app = Elm.PortSubCmdDemoMinimal.init({ node });
app.ports.getFromLocalStorage.subscribe(({debug}) => {
console.log("app.ports.getFromLocalStorage:", debug);
app.ports.gotTextFromLocalStorage.send("✅ fake text from LocalStorage");
});
</script>
</body>
</html>
Problem
If you:
portAinsubscriptionsportBinupdateat the same timeportBsends data toportA… will
portAget that data? Will it be subscribed in time?From an Elm programmer’s perspective it sure looks like the port will be subscribed and should get the data, if you read your
subscriptionsfunction (see the SSCCE at the bottom).The real answer is: It depends. And it depends on what order some generated JavaScript happens to end up in!
The really confusing thing for an Elm programmer is that if you try to debug this with
Debug.login the Elm code andconsole.login the JavaScript code (for the ports), it looks like things should be happening. You can see thatapp.ports.portA.send()is indeed called, but no message is ever seen on the Elm side, even though aDebug.loginsubscriptionsclearly shows that we do indeed callportA. And theDebug.loghappens before theconsole.logso the order looks correct, too. (Then in reality, things can happen in the opposite order, which explains why nothing happens.)Use case
I stumbled upon this at work. Our app has multiple pages. Each page has its own
Model,Msg,init,update,subscriptionsandview, and at the top level we delegate to the current page.On one page I wanted to get something from local storage when the page initializes. So in
initI call a port that reads from local storage, and replies on another port. InsubscriptionsI subscribe to that other port. Unfortunately, I never got a reply there. It turned out to be because I used the same local storage ports on other pages, and that affected the compiled code order.Why it happens
This is how Elm calls
updateandsubscriptions:It gives the
Cmds andSubs to_Platform_enqueueEffects, which passes them on to_Platform_dispatchEffects, which looks like this:It creates one single
effectsDictand calls_Platform_gatherEffectstwice – once for theCmds and once for theSubs._Platform_gatherEffectsassigns properties oneffectsDict. In this case the properties are the names of the ports.Then there’s a loop:
for (var home in managers). And inside the loop we read fromeffectsDict[home]. In this case, we have two ports. The order they are executed in comes down to the iteration order ofmanagers.managersis created in_Platform_setupEffects:Here there’s another loop:
for (var key in _Platform_effectManagers). We assign tomanagers[key]in the loop. So the iteration order ofmanagerscomes from the iteration order_Platform_effectManagers.In the case of ports, the
_Platform_outgoingPortand_Platform_incomingPortfunctions assign_Platform_effectManagers[name](wherenameis the port name):Those functions are called by generated code which can look like so (taken from the below SSCCE):
Those lines look like they simply define something at first glance, but remember that they also have the side effect of mutating
_Platform_effectManagers. And iteration order of JavaScript objects is based on the insertion order.Generated definitions seem to be ordered topologically or something. The order is not guaranteed. So depending on where you call those ports, the order can shift.
This means that if you use a port in a new place, you can accidentally break a usage of that port in a completely different part of the app.
Solution?
Process subscriptions before commands, so that the subscriptions are ready for the commands producing data.
SSCCE