Dependency injection across initializers


#1

I have an initializer that subscribes to Pusher events that tell when a model is updated on the server, and updates it on the client correspondingly. Here I have it set up to update the player and team models:

export default {
  name: 'pusher-updates',
  after: 'store',

  initialize: function(container) {
    var store = container.lookup('store:main');
    var pusherConnection = new window.Pusher(AppENV.pusherKey);
    var mainChannel = pusherConnection.subscribe('main-updates');

    mainChannel.bind('update-player', function(data) {
      store.update('player', data.player);
    });

    mainChannel.bind('update-team', function(date) {
      store.update('team', data.team);
    });
  }
};

This all worked very fine, but then I needed to update the current user. For my other models, I want to broadcast model changes to every client, but for user model updates, I only want to broadcast updates to the client for that particular user.

The current user is set up in an initializer of its own:

import Ember from 'ember';
import Session from 'simple-auth/session';

var SessionWithCurrentUser = Session.extend({
  currentUser: function() {
    var userId = this.get('id');
    if (!Ember.isEmpty(userId)) {
      var store = this.container.lookup('store:main');
      return store.find('user', userId);
    }
  }.property('id')
});

export default {
  name: 'customize-session',
  initialize: function(container) {
    container.register('session:withCurrentUser', SessionWithCurrentUser);
  }
};

But now, the customize-session initializer needs access to pusherConnection. My initial thought was to create another initializer that sets up the Pusher connection and injects it into the application. But where would it inject it to, when it needs to be accessed by other initializers? The Ember docs say “Injections can be made onto all of Ember’s major framework classes, including views, helpers, components, controllers, routes, and the router.” Is there a way to inject into initializers?

For now, I hacked it and made pusherConnection a property on window so that I could access it from the second initializer, like this:

var SessionWithCurrentUser = Session.extend({
  currentUser: function() {
    var userId = this.get('id');
    if (!Ember.isEmpty(userId)) {
      var store = this.container.lookup('store:main');
      var user = store.find('user', userId);
      var userChannel = window.pusherConnection.subscribe('user-' + userId);
      userChannel.bind('update-user', function(data) {
        store.update('user', data.user);
      });
      return user;
    }
  }.property('id')
});

But that just feels dirty. What’s the right way to do this?


#2

Hello michaelrkn,

I think the most ‘emberish’ approach would be to wrap your PusherConnection into a simpele EmberObject that you then register on the container inside your push-update initializer. In your customize-session initializer you could then simply container.lookup your PusherConnection object and set in on your session object. Something like that:

export default {
   name: 'customize-session',
   initialize: function(container) {
     var pusherConnection = this.container.lookup('pusher:connection');
   
    SessionWithCurrenUser.reopen({
      init: function () {
        this._super.apply(this, arguments);
        this.set('connection', pusherConnection);
      }
    });
 container.register('session:withCurrentUser', SessionWithCurrentUser);

I hope that helps. I’m very curious with what kind of solution you come up in the end :smiley:


#3

It is dirty indeed :smile:

Initializers should only be used to manage dependency injection. You are trying to use the initializer itself as a service, which is not a good pattern. Instead, split the service off:

// app/services/pusher
export default Ember.Object.extend({

  pusher: function(){
    return new window.Pusher(AppENV.pusherKey);
  }.property(),

  channel: function(){
    return pusherConnection.subscribe('main-updates');
  }.property(),

  listenForUpdates: function(){
    var channel = this.get('channel');
    var store = this.get('store'); // Provided by an injection

    channel.bind('update-player', function(data) {
      Ember.run(function(){ // See the runloop guide
        store.update('player', data.player);
      });
    });

   channel.bind('update-team', function(date) {
      Ember.run(function(){ // See the runloop guide
        store.update('team', data.team);
      });
    });
  }.on('init')

});
// app/initializers/pusher-updates
export default {
  name: 'pusher-updates',
  after: 'store',

  initialize: function(container, application) {
    application.inject('service:pusher', 'store', 'store:main');
    // This is the only hack. Force the service to instantiate
    container.lookup('service:pusher');
  }
};

The hack is required only because this service is not consumed anywhere. If it had properties that were displayed in a template or accessed in a route (for example), it would be lazily instantiated for you. But this is not the case so we must instantiate it ourselves.

Additionally, your session manager should also use dependency injection. In your current example you have:

var store = this.container.lookup('store:main');

This is bad for two reasons. It couples your code to the container API, which is not a public API, and it makes this code harder to test. Dependency injections are really just properties passed in to the object at creation time, so in tests you can just pass in stubs. Add this to your initializer for the custom sessions:

application.inject('session:withCurrentUser', 'store', 'store:main');

And now you can access the store as this.store on the session service. This initializer would also be where you should inject the session service onto other parts of the framework, such as routes and controllers.


#4

@maxn and @mixonic, thanks for the suggestions. My current thinking is to:

1, create a pusherConnection service that handles setting up the connection to Pusher, something like:

// services/pusher-connection.js
export default Ember.Object.extend({
  pusherConnection: function(){
    return new window.Pusher(AppENV.pusherKey);
  }.property()
});

2, use it to subscribe to the updates channel:

// initializers/pusher-updates.js
export default {
  name: 'pusher-updates',
  after: 'store',

  initialize: function(container) {
    var store = container.lookup('store:main');
    var pusherConnection = // how do i get access to the service?
    var mainChannel = pusherConnection.subscribe('main-updates');

    mainChannel.bind('update-player', function(data) {
      store.update('player', data.player);
    });

    mainChannel.bind('update-team', function(data) {
      store.update('team', data.team);
    });
  }
};

3, use it to subscribe to the current user channel:

// initializers/customize-session.js
import Ember from 'ember';
import Session from 'simple-auth/session';

var SessionWithCurrentUser = Session.extend({
  currentUser: function() {
    var userId = this.get('id');
    if (!Ember.isEmpty(userId)) {
      var store = this.container.lookup('store:main');
      var user = store.find('user', userId);
      var pusherConnection = // how do i get access to the service?
      var userChannel = pusherConnection.subscribe('user-' + userId);
      userChannel.bind('update-user', function(data) {
        store.update('user', data.user);
      });
      return user;
    }
  }.property('id')
});

export default {
  name: 'customize-session',
  after: 'pusher-connection',
  initialize: function(container) {
    container.register('session:withCurrentUser', SessionWithCurrentUser);
  }
};

But as you can see, I’m not sure how I get access to the service in the initializers. How does that part work? Am I missing something between steps 1 and 2?

@mixonic, you seem to be suggesting that I subscribe to the main-updates channel in the pusherConnection service, but my concern is that I may end up with many channels that I need to subscribe to (eg updates, creates, and destroys), and it feels like I’m cramming different things - the connection and the subscriptions - into a single service object. If an initializer shouldn’t be used to set up the subscription to the main-updates channel, and I’m right about keeping the subscription setup separate from the connection setup, where should I set up the subscriptions?

And @mixonic, I’ll work on switching access to the store to use DI - thanks for the suggestion.

Thanks a bunch!