Skip to content
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

BUG: concurrent state's child state is never entered #38

Closed
troygoode opened this issue Apr 19, 2012 · 16 comments
Closed

BUG: concurrent state's child state is never entered #38

troygoode opened this issue Apr 19, 2012 · 16 comments

Comments

@troygoode
Copy link

Hey Evin - I may be missing something, but I've stripped this example as far down as I could and still am able to reproduce this issue. Any thoughts?

var statechart;
if(require) // I'm using node to make this easier to test
  statechart = require('./stativus').createStatechart();
else
  statechart = Stativus.createStatechart();

////////////////////////////////////////////
// MY STATES:
// application
// - page (#nav and #content are concurrent)
//   - page#nav
//     - page#nav#first <-- BUG, NEVER LOADS
//   - page#content
////////////////////////////////////////////

statechart.addState('application', {
});

statechart.addState('page', {
  parentState: 'application'
  , substatesAreConcurrent: true
  , states: [
    {
      name: 'page#nav'
      , enterState: function(){ console.log('nav loaded'); } //<-- fires
      , states:[
        {
          name: 'page#nav#first'
          , enterState: function(){ console.log('nav.1 loaded'); } //<-- never fires
        }
      ]
    },
    {
      name: 'page#content'
      , enterState: function(){ console.log('content loaded'); } //<-- fires
    }
  ]
});

statechart.initStates('page#nav#first');
console.log(statechart.currentState().map(function(state){ return state.name; }));
// the above line should print something like:
//  ['page#nav#first', 'page#nav', 'page#content', 'page', 'application']
// instead, it prints:
//  ['page#nav', 'page#content', 'page', 'application']
// WHERE IS 'page#nav#first' ???
@sevifives
Copy link

Not really a bug:

var statechart;
if(require) // I'm using node to make this easier to test
statechart = require('./stativus').createStatechart();
else
statechart = Stativus.createStatechart();

////////////////////////////////////////////
// MY STATES:
// application
// - page (#nav and #content are concurrent)
// - page#nav
// - page#nav#first <-- BUG, NEVER LOADS
// - page#content
////////////////////////////////////////////

statechart.addState('application', {
initialSubstate: 'page'
});

statechart.addState('page', {
parentState: 'application'
, substatesAreConcurrent: true
, states: [
{
name: 'page#nav',
initialSubstate: 'page#nav#first'
, enterState: function(){ console.log('nav loaded'); } //<-- fires
, states:[
{
name: 'page#nav#first'
, enterState: function(){ console.log('nav.1 loaded'); } //<-- never fires
}
]
},
{
name: 'page#content'
, enterState: function(){ console.log('content loaded'); } //<-- fires
}
]
});

statechart.initStates({'default': 'application'});
console.log(statechart.currentState().map(function(state){ return state.name; }));

/**

[~/Desktop]$ node statey.js rvm:ruby-1.9.3-p125
ENTER: application
ENTER: page
ENTER: page#nav
nav loaded
ENTER: page#nav#first
nav.1 loaded
ENTER: page#content
content loaded
[ 'page#nav#first',
'page#nav',
'page#content',
'page',
'application' ]
**/

@troygoode
Copy link
Author

Yes, this works when you're using initialSubstate to point directly to a parallel substate's substate, but you're out of luck if you can't. See modified example:

var statechart;
if(require) // I'm using node to make this easier to test
  statechart = require('./stativus').createStatechart();
else
  statechart = Stativus.createStatechart();

////////////////////////////////////////////
// MY STATES:
// application
// - login
// - page (#nav and #content are concurrent)
//   - page#nav
//     - page#nav#first <-- BUG, NEVER LOADS
//   - page#content
////////////////////////////////////////////

statechart.addState('application', {
  initialSubstate: 'login'
});

statechart.addState('login', {
  parentState: 'application'
  , enterState: function(){ console.log('login loaded'); } //<-- fires
  , login_success: function(){ this.goToState("page#nav#first"); }
})

statechart.addState('page', {
  parentState: 'application'
  , substatesAreConcurrent: true
  , states: [
    {
      name: 'page#nav'
      , enterState: function(){ console.log('nav loaded'); } //<-- fires
      , states:[
        {
          name: 'page#nav#first'
          , enterState: function(){ console.log('nav.1 loaded'); } //<-- NEVER fires
        }
      ]
    },
    {
      name: 'page#content'
      , enterState: function(){ console.log('content loaded'); } //<-- fires
    }
  ]
});

statechart.initStates('application');
statechart.sendEvent('login_success');
console.log(statechart.currentState().map(function(state){ return state.name; }));
// the above line should print something like:
//  ['page#nav#second', 'page#nav', 'page#content', 'page', 'application']
// instead, it prints:
//  ['page#nav', 'page#content', 'page', 'application']
// WHERE IS 'page#nav#first' ???

@etgryphon
Copy link
Owner

As per @sevifives comments...

@troygoode
Copy link
Author

did you run the second repro scenario that uses initialSubstate and still doesn't work? still a bug

@sevifives
Copy link

var statechart;
if(require) // I'm using node to make this easier to test
statechart = require('./stativus').createStatechart();
else
statechart = Stativus.createStatechart();

////////////////////////////////////////////
// MY STATES:
// application
// - login
// - page (#nav and #content are concurrent)
// - page#nav
// - page#nav#first <-- BUG, NEVER LOADS
// - page#content
////////////////////////////////////////////

statechart.addState('application', {
initialSubstate: 'login'
, substatesAreConcurrent: true // need this
});

statechart.addState('login', {
parentState: 'application'
, enterState: function(){ console.log('login loaded'); } //<-- fires
, login_success: function(){ this.goToState("page#nav#first"); }
})

statechart.addState('page', {
parentState: 'application'
, substatesAreConcurrent: true
, states: [
{
name: 'page#nav'
, initialSubstate: 'page#nav#first' // need this
, enterState: function(){ console.log('nav loaded'); } //<-- fires
, states:[
{
name: 'page#nav#first'
, enterState: function(){ console.log('nav.1 loaded'); }
}
]
},
{
name: 'page#content'
, enterState: function(){ console.log('content loaded'); } //<-- fires
}
]
});

statechart.initStates({'default':'application'}); // this works
statechart.sendEvent('login_success');
console.log(statechart.currentState().map(function(state){ return state.name; }));

ENTER: application
ENTER: login
login loaded
ENTER: page
ENTER: page#nav
nav loaded
ENTER: page#nav#first
nav.1 loaded
ENTER: page#content
content loaded
EVENT: login fires [login_success] with 0 argument(s)
EXIT: login
ENTER: page
ENTER: page#nav
nav loaded
ENTER: page#content
content loaded
[ 'page#nav', 'page#content', 'page', 'application' ]

@troygoode
Copy link
Author

yes, see how "ENTER: page#nav#first" is never logged and the array at the end is missing "page#nav#first"?

@etgryphon
Copy link
Owner

@troygoode it is entered (look at the 5th ENTER: from the top)...I don't understand why it isn't in the array at the end..

@sevifives
Copy link

I've never tried to jump to the substate of another state. That seems a little odd to me.

var statechart;
if(require) // I'm using node to make this easier to test
  statechart = require('./stativus').createStatechart();
else
  statechart = Stativus.createStatechart();

////////////////////////////////////////////
// MY STATES:
// application
// - login
// - page (#nav and #content are concurrent)
//   - page#nav
//     - page#nav#first <-- BUG, NEVER LOADS
//   - page#content
////////////////////////////////////////////

statechart.addState('application', {
  initialSubstate: 'login'
});

statechart.addState('login', {
  parentState: 'application'
  , enterState: function(){ console.log('login loaded'); } //<-- fires
  , login_success: function(){ this.goToState("page"); }
})

statechart.addState('page', {
  parentState: 'application'
  , initialSubstate: 'page#nav'
  , substatesAreConcurrent: true
  , states: [
    {
      name: 'page#nav'
      , initialSubstate: 'page#nav#first'
      , enterState: function(){ console.log('nav loaded'); } //<-- fires
      , states:[
        {
          name: 'page#nav#first'
          , enterState: function(){ console.log('nav.1 loaded'); } //<-- NEVER fires
        }
      ]
    },
    {
      name: 'page#content'
      , enterState: function(){ console.log('content loaded'); } //<-- fires
    }
  ]
});

statechart.initStates({'default':'application'});
statechart.sendEvent('login_success');
console.log(statechart.currentState().map(function(state){ return state.name; }));

ENTER: application
ENTER: login
login loaded
EVENT: login fires [login_success] with 0 argument(s)
EXIT: login
ENTER: page
ENTER: page#nav
nav loaded
ENTER: page#nav#first
nav.1 loaded
ENTER: page#content
content loaded
[ 'page#nav#first',
'page#nav',
'page#content',
'page',
'application' ]

@troygoode
Copy link
Author

@sevifives, initialSubstate is not an okay solution to this:

var statechart;
if(require) // I'm using node to make this easier to test
  statechart = require('./stativus').createStatechart();
else
  statechart = Stativus.createStatechart();

////////////////////////////////////////////
// MY STATES:
// application
// - login
// - page (#nav and #content are concurrent)
//   - page#nav
//     - page#nav#first <-- BUG, NEVER LOADS
//   - page#content
////////////////////////////////////////////

statechart.addState('application', {
  initialSubstate: 'login'
});

statechart.addState('login', {
  parentState: 'application'
  , enterState: function(){ console.log('login loaded'); } //<-- fires
  , login_success: function(){ this.goToState("page#nav#second"); } // <-- doesn't work
  //, login_success: function(){ this.goToState("page"); } // <-- fires page#nav#first
})

statechart.addState('page', {
  parentState: 'application'
  , substatesAreConcurrent: true
  , initialSubstate: 'page#nav'
  , states: [
    {
      name: 'page#nav'
      , enterState: function(){ console.log('nav loaded'); } //<-- fires
      , initialSubstate: 'page#nav#first'
      , states:[
        {
          name: 'page#nav#first'
          , enterState: function(){ console.log('nav.1 loaded'); } //<-- fires
        },
        {
          name: 'page#nav#second'
          , enterState: function(){ console.log('nav.2 loaded'); } //<-- NEVER fires
        }
      ]
    },
    {
      name: 'page#content'
      , enterState: function(){ console.log('content loaded'); } //<-- fires
    }
  ]
});

statechart.initStates({'default': 'application'});
statechart.sendEvent('login_success');
console.log(statechart.currentState().map(function(state){ return state.name; }));
// the above line should print something like:
//  ['page#nav#second', 'page#nav', 'page#content', 'page', 'application']
// instead, it prints:
//  ['page#nav', 'page#content', 'page', 'application']
// WHERE IS 'page#nav#second' ???

ENTER: application
ENTER: login
login loaded
EVENT: login fires [login_success] with 0 argument(s)
EXIT: login
ENTER: page
ENTER: page#nav
nav loaded
ENTER: page#content
content loaded
[ 'page#nav', 'page#content', 'page', 'application' ]

@sevifives
Copy link

This is how I see it:

var statechart;
if(require) // I'm using node to make this easier to test
  statechart = require('./stativus').createStatechart();
else
  statechart = Stativus.createStatechart();

statechart.addState('application', {
  initialSubstate: 'login',
  substatesAreConcurrent: true
});

statechart.addState('login', {
  parentState: 'application'
  , enterState: function(){ console.log('login loaded'); }
  , verify_this: function () {
    statechart.sendEvent('login_success');
  }
})

statechart.addState('page', {
  parentState: 'application'
  , initialSubstate: 'noPage'
  , states: [
    {
      name: 'noPage'
      , login_success: function () {
        this.goToState('page#nav');
      }
    },
    {
      name: 'page#nav'
      , initialSubstate: 'page#nav#first'
      , enterState: function(){ console.log('nav loaded'); }
      , states:[
        {
          name: 'page#nav#first'
          , enterState: function(){ console.log('nav.1 loaded'); }
        }
      ]
    },
    {
      name: 'page#content'
      , enterState: function(){ console.log('content loaded'); }
    }
  ]
});

statechart.initStates({'default':'application'});
statechart.sendEvent('verify_this');
console.log(statechart.currentState().map(function(state){ return state.name; }));

ENTER: application
ENTER: login
login loaded
ENTER: page
ENTER: noPage
EVENT: login fires [verify_this] with 0 argument(s)
EVENT: noPage fires [login_success] with 0 argument(s)
EXIT: noPage
ENTER: page#nav
nav loaded
ENTER: page#nav#first
nav.1 loaded
EVENT: login_success with 0 argument(s) found NO state to handle the event
[ 'login', 'page#nav#first', 'page#nav', 'application' ]

@troygoode
Copy link
Author

Not an acceptable solution. What do you do when page and login are not concurrent? Those were just example states, don't focus on the names.

Edit: also, that code is very spaghetti

@etgryphon etgryphon reopened this Apr 19, 2012
@etgryphon
Copy link
Owner

something doesn't look right...i'll look into it.

@troygoode
Copy link
Author

This code is closer to my real-life need:

https://gist.github.com/2421138

@troygoode
Copy link
Author

I just discovered this (likely) doesn't have anything to do with concurrent states, but is actually a problem with attempting to goToState where your target is a child of a sibling state (nephew/niece state?):

var Stativus = require('./stativus');
var statechart = Stativus.createStatechart();

statechart.addState('application', {
  initialSubstate: 'page1'
});

statechart.addState('page1', {
  parentState: 'application'
  , go: function(){ this.goToState('page2.2'); }
});

statechart.addState('page2', {
  parentState: 'application'
  , initialSubstate: 'page2.1'
});

statechart.addState('page2.1', {
  parentState: 'page2'
});
statechart.addState('page2.2', {
  parentState: 'page2'
});

statechart.initStates('application');
statechart.sendEvent('go');
console.log(statechart.currentState().map(function(state){ return state.name; }));

// expect to see [ 'page2.2', 'page2', 'application'], instead I get:
/*
troy:~/dev/foo $ node test.js 
ENTER: application
ENTER: page1
EVENT: page1 fires [go] with 0 argument(s)
EXIT: page1
ENTER: page2
[ 'page2', 'application' ]
*/

@etgryphon
Copy link
Owner

@troygoode great catch...this only happens when you have a single parent with child states as the first state. Technically your code should look like this:

var Stativus = require('./stativus');
var statechart = Stativus.createStatechart();

statechart.addState('page1', {
 , go: function(){ this.goToState('page2.2'); }
});

statechart.addState('page2', {
 , initialSubstate: 'page2.1'
});

statechart.addState('page2.1', {
 parentState: 'page2'
});
statechart.addState('page2.2', {
 parentState: 'page2'
});

statechart.initStates('page1');
statechart.sendEvent('go');
console.log(statechart.currentState().map(function(state){ return state.name; }));

/*
troy:~/dev/foo $ node test.js 
ENTER: page1
EVENT: page1 fires [go] with 0 argument(s)
EXIT: page1
ENTER: page2
ENTER: page2.2
[ 'page2.2','page2', 'application' ]
*/

@etgryphon
Copy link
Owner

i fixed the code so you can do the code that you wrote which should be permissible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants