New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Potential zalgo in ports #712

Closed
wishfoundry opened this Issue Sep 15, 2016 · 3 comments

Comments

Projects
None yet
3 participants
@wishfoundry

wishfoundry commented Sep 15, 2016

if more than 1 subscriber exists on a port, and if that port attempts to unsubscribe while in an event, there is potential to skip the next listener on that port

https://github.com/elm-lang/core/blob/master/src/Native/Platform.js#L486

@process-bot

This comment has been minimized.

Show comment
Hide comment
@process-bot

process-bot Sep 15, 2016

Thanks for the issue! Make sure it satisfies this checklist. My human colleagues will appreciate it!

Here is what to expect next, and if anyone wants to comment, keep these things in mind.

process-bot commented Sep 15, 2016

Thanks for the issue! Make sure it satisfies this checklist. My human colleagues will appreciate it!

Here is what to expect next, and if anyone wants to comment, keep these things in mind.

@lukewestby

This comment has been minimized.

Show comment
Hide comment
@lukewestby

lukewestby Sep 20, 2016

Member

I dug into this a little bit:

This happens because subs are iterated in a for loop when a message comes in:

https://github.com/elm-lang/core/blob/master/src/Native/Platform.js#L462-L465

for (var i = 0; i < subs.length; i++)
{
  subs[i](value);
}

If there are 3 callbacks in subs, and subs[1] removes itself from the list in place by unsubscribing, then subs[2] becomes subs[1] and the for loop ends because i is now equal to subs.length but than final callback has never been called. To avoid this other systems that employ a list of subscribers will make a copy of the subscribers array with Array.prototype.slice() at opportune moments. The assumption there is that the subscribers array is generally small so that copying is fast, and I think that's a fair assumption here as well.

Member

lukewestby commented Sep 20, 2016

I dug into this a little bit:

This happens because subs are iterated in a for loop when a message comes in:

https://github.com/elm-lang/core/blob/master/src/Native/Platform.js#L462-L465

for (var i = 0; i < subs.length; i++)
{
  subs[i](value);
}

If there are 3 callbacks in subs, and subs[1] removes itself from the list in place by unsubscribing, then subs[2] becomes subs[1] and the for loop ends because i is now equal to subs.length but than final callback has never been called. To avoid this other systems that employ a list of subscribers will make a copy of the subscribers array with Array.prototype.slice() at opportune moments. The assumption there is that the subscribers array is generally small so that copying is fast, and I think that's a fair assumption here as well.

@lukewestby lukewestby added the bug label Sep 20, 2016

@lukewestby

This comment has been minimized.

Show comment
Hide comment
@lukewestby

lukewestby Sep 20, 2016

Member

I've already started a fix, but in the mean time here's an sscce

Main.elm

port module Main exposing (..)

import Html
import Html.App
import Time exposing (Time)


type alias Model =
    ()


port test : Time -> Cmd msg


type Msg
    = Tick Time


update : Msg -> Model -> ( Model, Cmd Msg )
update (Tick time) _ =
    ( ()
    , test time
    )


subscriptions : Model -> Sub Msg
subscriptions _ =
    Time.every Time.second Tick


main : Program Never
main =
    Html.App.program
        { view = \_ -> Html.text ""
        , update = update
        , subscriptions = subscriptions
        , init = ( (), Cmd.none )
        }

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <script src="out.js"></script>
    <script>
      var app = Elm.Main.fullscreen();
      app.ports.test.subscribe(function unsub(time) {
        console.log('first sub got called with', time);
        app.ports.test.unsubscribe(unsub);
      });
      app.ports.test.subscribe(function (time) {
        console.log('second sub got called with', time);
      });
    </script>
  </body>
</html>

You should see 'first sub got called with <time A>' and not 'second sub got called with <time A>', and then it should continue with 'second sub got called with <time N>'

Member

lukewestby commented Sep 20, 2016

I've already started a fix, but in the mean time here's an sscce

Main.elm

port module Main exposing (..)

import Html
import Html.App
import Time exposing (Time)


type alias Model =
    ()


port test : Time -> Cmd msg


type Msg
    = Tick Time


update : Msg -> Model -> ( Model, Cmd Msg )
update (Tick time) _ =
    ( ()
    , test time
    )


subscriptions : Model -> Sub Msg
subscriptions _ =
    Time.every Time.second Tick


main : Program Never
main =
    Html.App.program
        { view = \_ -> Html.text ""
        , update = update
        , subscriptions = subscriptions
        , init = ( (), Cmd.none )
        }

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <script src="out.js"></script>
    <script>
      var app = Elm.Main.fullscreen();
      app.ports.test.subscribe(function unsub(time) {
        console.log('first sub got called with', time);
        app.ports.test.unsubscribe(unsub);
      });
      app.ports.test.subscribe(function (time) {
        console.log('second sub got called with', time);
      });
    </script>
  </body>
</html>

You should see 'first sub got called with <time A>' and not 'second sub got called with <time A>', and then it should continue with 'second sub got called with <time N>'

evancz added a commit that referenced this issue Sep 20, 2016

Merge pull request #715 from elm-lang/luke/port_unsubscribe_drops_sub
Fix for #712 - don't skip next sub if previous sub unsubscribes itself

@lukewestby lukewestby closed this Sep 20, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment