Nils Landt Nils Landt - 7 days ago 5
CoffeeScript Question

Ember computed property depending on service property not updating

In my Ember 2.8 application, I'm establishing a Websocket connection in a service. The connection URL changes when a user is logged in (it then includes the user auth token as a query parameter).

The current user service is simple:

CurrentUserService = Ember.Service.extend(
name: "current-user"

user: null

load: ->
// Do some stuff
@set("user", user)
)


It works exactly as expected, and I use it to display the current users username on the page (among other things).

In the Websocket service, all I do is create a computed property, depending on currentUser.user, that sets up the connection (depending on whether a user is logged in):

ActionCableService = Ember.Service.extend(
name: "action-cable"

cable: service()
currentUser: service()

testObs: Ember.observer("currentUser", ->
console.log "currentUser changed, #{ @get("currentUser.user") }"
)

consumer: Ember.computed("currentUser.user", ->
consumerUrl = "ws://localhost:10000/cable"

if @get("currentUser").user?
consumerUrl += "?token=#{ @get("currentUser.user.authToken") }"

console.log(consumerUrl)
return @get("cable").createConsumer(consumerUrl)
)
)


Problem is, the consumer property never gets updated. It's set once, on page load, and when the user property of the currentUser service changes,
consumer
is not updated, and neither does my test observer.

When I refresh the page, sometimes the logged in consumerUrl is used, and sometimes it's not.

I'm guessing sometimes the session restoration happens first, and sometimes the action cable service happens first.

What I expected to happen when the action cable service gets loaded first is:


  1. Action cable service gets loaded, no current user set yet, connect to public websocket

  2. Logic that handles restoring user from session data fires, sets
    currentUser.user
    (this happens, I can see the username on my page)

  3. The
    consumer
    computed property notices the
    currentUser.user
    change and connects to the private consumerUrl (does not happen)



I can very easily solve this problem in a way that does not depend on computed properties, but I would like to know what went wrong here.

Answer

Computed properties, by default, observe any changes made to the properties they depend on, and are dynamically updated when they're called.

Unless you are invoking or calling that computed property, it will not execute your intended code.

Observers, on the other hand, react without invocation, when the property they are watching, changes. But they are often overused, and can easily introduce bugs due to their synchronous nature.

You could refactor your observers and computed properties into helper functions that are called directly. This makes them easier to unit test as well.

In your controller, you can handle the initial action of logging in, like this:

currentUser: Ember.inject.service(),

actions: {
  login() {
    this.auth({ username: 'Mary' });
  },
},

auth(data) {
  // Send data to server for authentication...

  // ...upon response, handle the following within the promise's `then`
  // method, failures caught within `catch`, etc. But for purposes of 
  // demonstration, just mocking this for now...
  const response = { username: 'Mary', authToken: 'xyz', };
  this.get('currentUser').setConsumer(response);
},

The current-user service could then set it’s properties, and call a helper function on the action-cable service:

actionCable: Ember.inject.service(),

authToken: null,
username: null,

setConsumer(response) {
  this.set('authToken', response.authToken);
  this.set('username', response.username);
  this.get('actionCable').setConsumer();
},

The action-cable service reads properties from currentService, sets the consumerUrl, and calls the cable service to create the consumer:

cable: Ember.inject.service(),
currentUser: Ember.inject.service(),

setConsumer() {
  var consumerUrl = "ws://localhost:10000/cable";

  if (this.get("currentUser.username") !== null) {
    consumerUrl += "?token=" + (this.get("currentUser.authToken"));
  }

  console.log("ACTION CABLE SERVICE, Consumer URL: ", consumerUrl);
  this.get("cable").createConsumer(consumerUrl);
}

I’ve created an Ember Twiddle to demonstrate.

Comments