Skip to content

Instantly share code, notes, and snippets.

@BlakeWilliams
Last active August 29, 2015 14:16
Show Gist options
  • Select an option

  • Save BlakeWilliams/5c22fed5a5609a6d22ce to your computer and use it in GitHub Desktop.

Select an option

Save BlakeWilliams/5c22fed5a5609a6d22ce to your computer and use it in GitHub Desktop.

Elixir Channel UUID's

Elixir Channel's don't provide a way to associate a request with a response, which means it's not possible to reliably pair requests to responses.

The problem

I'm attempting to write an Ember-Data adapter for Phoenix websockets and Ember-Data is heavily built on promises and on a request/response lifecycle.

For example, when implementing the find-all function with the existing way channels are handled we run into a problem, find-all expects an array of records or a promise that will resolve into an array of records.

Currently the only way I can imagine doing this with the current implementation is to make requests, return a promise that will (eventually) resolve by using the first response received with a matching event. This is fine in a simple reply example, but starts to break down when creating multiple records at the same time (particularly of the same type) or dealing with broadcasts (since multiple users creating records and then being broadcasted could break the order, and cause weird issues).

How the RESTAdapter handles this

  findAll: function(store, type, sinceToken) {
    var query;

    if (sinceToken) {
      query = { since: sinceToken };
    }

    return this.ajax(this.buildURL(type.typeKey), 'GET', { data: query });
  },

this.ajax will return a promise that will either resolve into an array of objects or reject with the appropriate errors. The only way to handle this now with Phoenix is to return an empty set of data and let event update the data when it's received.

Ember-Data uses this promise to determine if the data is loaded (if this is the first request), routes use it to show the loading route until the promise resolves, and it doesn't allow us to determine if there are no records loaded yet or if the returned data was empty (since Ember-Data forces us to return a promise or an array of records).

How I propose phoenix.js would solve this

Here's a high level example of how I think the public API would look:

  findAll: function(store, type, sinceToken) {
    // phoenix is an instance of Phoenix injected by the Ember container
    return new Ember.RSVP.Promise(function(resolve, reject) {
      this.phoenix.send(`#{type.typeKey}:index`, {}).then(function(data) {
        const responseData = JSON.parse(event.data);
        resolve(responseData.payload);
      }, function(data) {
        //reject with the error
      });
    })
  },

We could achieve this by generating and sending a UUID in each request and responding with the same UUID in the matching response.

This would make the websocket request/response go from this:

{"topic":"announcements","event":"announcements:index","payload":{}}	
{"topic":"announcements","payload":{"announcements":[]}},"event":"announcements:index"}	

to this:

{"uuid": "123abc", "topic":"announcements","event":"announcements:index","payload":{}}	
{"uuid": "123abc", "topic":"announcements","payload":{"announcements":[]}},"event":"announcements:index"}	

"Under the hood" we could create and return a promise that resolves/rejects once a response with the same UUID is received.

Buy Why UUID's?

In IRC it was mentioned that using an ack response could solve this issue partially, which is true. This only falls apart when you make multiple requests to the same topic:event at the same time. If some fail and some succeed, how can you reliably determine which requests failed and which succeeded? If the responses come out of order, you're out of luck.

When using a UUID you could store an object of promises with the keys being the UUID's and the promises's resolve and reject functions being the value. When receiving replies this works great, and when receiving a broadcast we could look for this UUID and resolve it if it exists, otherwise handle it manually.

eg: { "123abc": { resolve: resolveFunctionHere, reject: rejectFunctionHere } }

This would all be transparent to handle_in and other events, and just be passed straight through each request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment