This idea of ‘behavior as coded rather than behavior as documented’ is familiar to me from the days when I used to write high performance messaging software.
We would code our system as a series of processing stages which could be combined together synchronously or asynchronously. Synchronous was usually better for low-latency which was the main use case, but async was better for throughput which was sometimes the more important use-case. It didn’t really matter either way - you still assumed the behaviour was async and coded for that expectation - that would work correctly in both scenarios.
I find in Elm I am very often implementing my update around a state machine, and tagging the messages against the states they are legal in:
type State =
Loading
| Ready
type Msg =
HttpResponse
| MouseMoved
update msg model =
case (msg, model.state) of
(HttpResponse, Loading) -> ...
(MouseMoved, Ready) -> ...
_ -> (model, Cmd.none) -- ignore as no-op
That way if multiple events are in flight at the same time, and an earlier one puts the state machine into a state that is not allowed for the later event, the later events will end up being ignored. I assume everything can be async, and use a state machine to restrict to the allowable models.