1

I would like to run some specific code around put() and add() for Dojo stores. The problem I am having is that for JSON REST stores, in JsonRest.js add() is just a function that calls put():

add: function(object, options){
  options = options || {};
  options.overwrite = false;
  return this.put(object, options);
},

So, if I use aspect.around() with add(), my code ends up being executed twice IF I apply my code to stores created with a store that implements add() as a stub to put().

Please note that I realise that most stores will do that. I just want my solution to be guaranteed to work with any store, whether there is method nesting or not.

Dojo's own Observable.js has the same problem. This is how they deal with it:

function whenFinished(method, action){
    var original = store[method];
    if(original){
      store[method] = function(value){
        if(inMethod){
          // if one method calls another (like add() calling put()) we don't want two events
          return original.apply(this, arguments);
        }
        inMethod = true;
        try{
          var results = original.apply(this, arguments);
          Deferred.when(results, function(results){
            action((typeof results == "object" && results) || value);
          });
          return results;
        }finally{
          inMethod = false;
        }
      };
    }
  }
  // monitor for updates by listening to these methods
  whenFinished("put", function(object){
    store.notify(object, store.getIdentity(object));
  });


  whenFinished("add", function(object){
    store.notify(object);
  });
  whenFinished("remove", function(id){
    store.notify(undefined, id);
  });

My question is: is there a simple, "short" way to change my existing code so that it checks if it's within a method, and avoid running the code twice?

I gave it a go, but I ended up with clanky, hacky code. I am sure I am missing something...

Here is my existing code:

topic.subscribe( 'hotplate/hotDojoStores/newStore', function( storeName, store ){

  aspect.around( store, 'put', function( put ){

    return function( object, options ){

      return when( put.call( store, object, options ) ).then( function( r ) {
        var eventName;
        var identity = store.idProperty;
        eventName = object[ identity ] ? 'storeRecordUpdate' : 'storeRecordCreate';

        topic.publish( eventName, null, { type: eventName, storeName: storeName, objectId: r[ identity ], object: object }, false );

      } );

    }
  });

  aspect.around( store, 'add', function( add ){
    return function( object, options ){

      return when( add.call( store, object, options ) ).then( function( r ) {

        var identity = store.idProperty;

        topic.publish('storeRecordCreate', null, { storeName: storeName, storeTarget: storeTarget, objectId: r[identity], object: object }, false }  );

      });
    }
  });
});
Merc
  • 16,277
  • 18
  • 79
  • 122
  • I follow the grain and do what Observable does with my store wrappers. There may be a "shorter" approach by checking if the [put caller is add](http://stackoverflow.com/questions/280389/how-do-you-find-out-the-caller-function-in-javascript) -- but I'm tired and there may need to be some stack gymnatics because of aspect that prevents it. – bishop Dec 23 '13 at 01:59
  • Ouch not the answer I was hoping... Can you have a look at my attempted answer? Does it look sane? – Merc Dec 23 '13 at 06:27

1 Answers1

0

This is my attempt... What I don't really "get" about my attempt is whether it's 100% safe or not.

If store.add() is called twice in a row, is is ever possible that inMethod is set to true by the first call, and that the second add() call then finds it already set to true because the first one hasn't managed to set it to false yet?

This would only really be possible if nextTick() is called in between the two calls I assume?

Or am I just completely confused by it all? (Which is very possible...)

  topic.subscribe( 'hotplate/hotDojoStores/newStore', function( storeName, store ){

    var inMethod;

    aspect.around( store, 'put', function( put ){

      return function( object, options ){

        if( inMethod ){
          return when( put.call( store, object, options ) );
        } else {

          inMethod = true;

          try {
            return when( put.call( store, object, options ) ).then( function( r ) {
              var eventName;
              var identity = store.idProperty;
              eventName = object[ identity ] ? 'storeRecordUpdate' : 'storeRecordCreate';

              topic.publish( eventName, null, { type: eventName, storeName: storeName, objectId: r[ identity ], object: object }, false );

            });
          } finally {
            inMethod = false;
          }

        }

      }
    });

    aspect.around( store, 'add', function( add ){
      return function( object, options ){

        if( inMethod ){
          return when( add.call( store, object, options ) );
        } else {

          inMethod = true;

          try {

            return when( add.call( store, object, options ) ).then( function( r ) {

              var identity = store.idProperty;

              topic.publish('storeRecordCreate', null, { type: 'storeRecordCreate', storeName: storeName, objectId: r[identity], object: object }, false );

            });
          } finally {
            inMethod = false;
          }
        }
      }

    });

    aspect.around( store, 'remove', function( remove ){
      return function( objectId, options ){

        return when( remove.call( store, objectId, options ) ).then( function( r ) {

          topic.publish('storeRecordRemove', null, { type: 'storeRecordRemove', storeName: storeName, objectId: objectId }, false );

        });
      };
    });

  });
Merc
  • 16,277
  • 18
  • 79
  • 122
  • 1
    If the two add() are back to back, they're in the same stack & therefore same event loop: so they won't get interleaved. The return in your try cannot complete until the promise resolves (because of the .then), so that will prevent Promise stacking. So while the code seems safe, why not refactor to the whenFinished() pattern established by Observable? From what I can tell, the only extension is publishing a topic after the method concludes. – bishop Dec 23 '13 at 15:27
  • Well it's about using Dojo, rather than inventing the wheel... Dojo provides `aspects`. So, why not use them, really? I think Observable does it "by hand" because it's old code... Plus, I might well need to put things "around" later on in my code. – Merc Dec 23 '13 at 22:38
  • 1
    Sure, ok. I'm just thinking that if the code looks worrisome it probably is in subtle ways that even a pair of eyes may miss. So how about Aspect.after() and a duplicate suppression filter on topic.publish? – bishop Dec 23 '13 at 23:30
  • Aspect.after is definitely a strong possibility. Duplicate suppression filter is unworkable :( Thanks for your help though! – Merc Dec 24 '13 at 03:00