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.
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).
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).
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.
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.