`Task.perform`/`Time.now` called once but firing twice

I’m trying to figure out a really weird bug. In some cases when I run Time.now, it seems like it consistently gets triggered twice and I can’t figure out any reason why.

Relevant code (I have a Timestamp data type in my app), so this is the context for the now function (with some Debug.log statements included):

module Utils.Types.Timestamp exposing (Timestamp(..), now)

type Timestamp
    = Timestamp Int

now : (Timestamp -> msg) -> Cmd msg
now msg =
    let
        _ =
            Debug.log "Timestamp.now msg" msg
    in
    Task.perform
        (\posix ->
            let
                _ =
                    Debug.log "perform msg" msg
            in
            msg (fromTime posix)
        )
        Time.now

fromTime : Posix -> Timestamp
fromTime time =
    time
        |> Time.posixToMillis
        |> Timestamp

When some parts of my code call this function, it prints what you would expect:

Timestamp.now msg: <function>
perform msg: <function>

But in others, this happens:

Timestamp.now msg: <function>
perform msg: <function>
perform msg: <function>

Timestamp.now is only being called once, but somehow Task.perform is getting triggered twice (if I print the resulting timestamp, I get two veeeery slightly different timestamps, like 1681681068968 vs 1681681068971). Is it possible there’s a bug in the Time.now or Task.perform implementations or something that causes this to sometimes trigger twice? Or is there another possibility I’m missing? I’m completely bewildered.

In Elm, tasks are executed not when Task.perform is called, but when the resulting command is returned from update (or init). Is it possible that you’re returning the same command more than once?

I can’t figure out how it would be, but will try and see if there’s a way it could be. That said, even in that case, wouldn’t we still expect Timestamp.now msg: <function> to be output twice as well?

No, the “Timestamp.now msg” would be printed when now is called and the command is created.

1 Like

So I think you turned out to be spot-on on this. I’ve been dealing with a lot of very strange both duplicating and disappearing Cmd bugs recently and it took me a while to figure out where they were coming from but I think I’ve finally figured it out. It was a weird ride.

what was the root cause in the end? Maybe we/others can learn from your findings.

So since I have some update function nesting in my app, I have some kind of weird update functions. Instead of returning a (Model, Cmd msg) tuple, they return what I’ve called a BubbleUp record that looks like this

type alias BubbleUp model msg instructions =
    { model : model
    , cmd : Cmd msg
    , bubble : BubbleUpData instructions
    }


type alias BubbleUpData instructions =
    { error : Maybe Report
    , modal : Maybe ModalType
    , instructions : Maybe instructions
    }

The BubbleUpData part gets passed from nested update functions up the the top update function so that, for example, all of the error reporting for the application happens in the same place at the very top.

And I wrote an andThen function for chaining BubbleUp record operations when I might need to apply several functions that may add changes to both the model and the Cmds. It currently looks like this:

andThen :
    (BubbleUp model msg instructions -> BubbleUp model msg instructions)
    -> BubbleUp model msg instructions
    -> BubbleUp model msg instructions
andThen bubbleUpFunc bubbleUp =
    let
        nextBubbleUp =
            bubbleUp
                |> withCmd Cmd.none
                |> bubbleUpFunc
    in
    { model = nextBubbleUp.model
    , cmd = Cmd.batch [ bubbleUp.cmd, nextBubbleUp.cmd ]
    , bubble =
        { error =
            bubbleUp.bubble.error
                |> Maybe.or nextBubbleUp.bubble.error
        , modal =
            bubbleUp.bubble.modal
                |> Maybe.or nextBubbleUp.bubble.modal
        , instructions =
            bubbleUp.bubble.instructions
                |> Maybe.or nextBubbleUp.bubble.instructions
        }
    }

So I might have an update function branch that’s like:

CloseHelpMenu ->
    model
    |> Model.closeHelpMenu
    |> BubbleUp.fromModel
    |> BubbleUp.withCmd (setFocus model.previouslyFocusedElement)
    |> BubbleUp.andThen doSomeOtherStuff

The tricky part is in how Cmds are handled. If the andThen function doesn’t have that |> withCmd Cmd.none line, then if doSomeOtherStuff doesn’t set a command of its own, that Cmd.batch [ bubbleUp.cmd, nextBubbleUp.cmd ] line will batch the setFocus model.previouslyFocusedElement Cmd with another copy of itself, doubling it up.

That was the main thing I had to figure out. The similar disappearing Cmds issue I had was happening because I was using andThen in some places and not others, so the places I was using it, everything was working fine, but the places I wasn’t, if a BubbleUp record got passed through a chain of functions and more than one of those functions called withCmd, only the very last Cmd that was set would actually get triggered, because without the batching in the andThen function, each call to withCmd just replaces whatever command was previously set.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.