Nils Landt
Nils Landt

Reputation: 3134

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.

Upvotes: 2

Views: 1360

Answers (2)

user8157522
user8157522

Reputation: 11

Another way would be to emit an event in the service and then subscribe to the event in the init method and set the value of the dependent key of the computed property to force it to be recomputed/updated.

Upvotes: 1

jacefarm
jacefarm

Reputation: 7431

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.

Upvotes: 3

Related Questions