Getting the latest data from a promise returning service called repeatedly
Asked Answered
A

1

3

I have an angular service that returns a promise for data obtained from a server. This data is requested multiple times in a fixed interval to keep the display up to date. The response can sometimes be quite slow (up to 10 seconds), and if two requests overlap and the first one responds last I'll get out of date information displayed in the app. The timelines would be something like:

- first request
Req ---------------------> Res
- second request
      Req -------> Res

Currently I keep a counter of the request and make the .then function close over it and discard the data if it is too old. I wonder if one of the promise libraries out there already does it or if there is a standard way to do it.

I've also though about adding the response timestamp to the returned object, or somehow using something like RxJs but I haven't used it to know if it applies somehow.

Ambur answered 3/10, 2016 at 3:57 Comment(5)
You can never be sure of which order the requests are finished in (unless ofcourse you actually implement that specifically on the server side), so your idea of a timestamp is probably the best solution.Unreconstructed
not sure a response timestamp would help ... in your example, wouldn't the first request get the later timestamp - depending on when the timestamp is added to the response of course - or the timestamp is the time of the requestHosmer
@JaromandaX thanks, I hadn't noticed that... but I could use the request time instead, after testing that later requests actually get newer info :SAmbur
I just added that suggestion to my comment :pHosmer
In a theoretical situation where the server is under load, and takes about 8 seconds to respond to your request, wouldn't this just increase its punishment over time? Asking "are we there yet" again won't give you a faster response. I feel like it would make more sense to wait 10 seconds after the response/timeout, not the request.Crosier
W
1

TL&DR: We are inventing cancellable promises here.

Well.. OK. Some infrastructure. This is a typical example where you really need Promise.cancel() However we don't have it in ES6 native promises. Being a library agnostic person i just go ahead and invent one by Promise sub-classing.

The following function takes a promise and makes it cancellable by adding a non-enumerable and non-configurable property called __cancelled__ It also adds .then() and .cancel() methods to it's property chain without modifying the Promise.prototype. Since cancellable promise object's proptotype's prototype is Promise.prototype, our cancellable promise has access to all Promise thingies. Ah.. before i forget; cancellable prototype's then method also returns a cancellable promise.

function makePromiseCancellable(p){
  Object.defineProperty(p,"__cancelled__", {        value: false,
                                                 writable: true,
                                               enumerable: false,
                                             configurable: false
                                           });
  Object.setPrototypeOf(p,makePromiseCancellable.prototype);
  return p;
}

makePromiseCancellable.prototype = Object.create(Promise.prototype);
makePromiseCancellable.prototype.then   = function(callback){
                                            return makePromiseCancellable(Promise.prototype.then.call(this,function(v){
                                                                                                             !this.__cancelled__ && callback(v);
                                                                                                           }.bind(this)));
                                          };
makePromiseCancellable.prototype.cancel = function(){
                                            this.__cancelled__ = true;
                                            return this;
                                          };

So we have a utility function called getAsyncData() which returns us a standard ES6 promise which resolves in 2000 msecs. We will obtain two promises from this function, and turn them into cancellable promises called cp0 and cp1. Then we will cancel cp0 at 1000 msecs and see what happens.

function getAsyncData(){
  var dur = 2000;
  return new Promise((v,x) => setTimeout(v.bind(this,"promise id " + pid++ + " resolved at " + dur + " msec"),dur));
}

function makePromiseCancellable(p){
  Object.defineProperty(p,"__cancelled__", {        value: false,
                                                 writable: true,
                                               enumerable: false,
                                             configurable: false
                                           });
  Object.setPrototypeOf(p,makePromiseCancellable.prototype);
  return p;
}

makePromiseCancellable.prototype = Object.create(Promise.prototype);
makePromiseCancellable.prototype.then   = function(callback){
                                            return makePromiseCancellable(Promise.prototype.then.call(this,function(v){
                                                                                                             !this.__cancelled__ && callback(v);
                                                                                                           }.bind(this)));
                                          };
makePromiseCancellable.prototype.cancel = function(){
                                            this.__cancelled__ = true;
                                          };
var pid = 0,
    cp0 = makePromiseCancellable(getAsyncData());
    cp1 = makePromiseCancellable(getAsyncData());
cp0.then(v => console.log(v));
cp1.then(v => console.log(v));

setTimeout(_ => cp0.cancel(),1000);

Wow..! fantastic. cp1 resolved at 2000 msec while cp0 has got cancelled at 1000 msecs.

Now, since we now have the infrastructure, we can use it to solve your problem.

The following is the code that we will use;

function getAsyncData(){
  var dur = ~~(Math.random()*9000+1001);
  return new Promise((v,x) => setTimeout(v.bind(this,"promise id " + pid++ + " resolved at " + dur + " msec"),dur));
}

function runner(fun,cb){
  var promises = [];
  return setInterval(_ => { var prom = makePromiseCancellable(fun());
                            promises.push(prom);
                            promises[promises.length-1].then(data => { promises.forEach(p => p.cancel());
                                                                       promises.length = 0;
                                                                       return cb(data);
                                                                     });
                          },1000);
}

var pid = 0,
    sid = runner(getAsyncData,v => console.log("received data:", v));
setTimeout(_=> clearInterval(sid),60001);

It's pretty basic. The runner() function is doing the job. It's requesting a promise every 1000msecs by invoking getAsyncData(). The getAsyncData() function however this time will give us a promise which will resolve in 1~10 seconds. This is so because we want some of the later promises to be able to resolve while some of the previously received ones are still in unresolved state. Just like in your case. OK; after making the received promise cancellable, the runner() function pushes it into the promises array. Only after pushing the promise to the promises array we attach the then instruction to it because we want the array to hold only the main promises, not the ones returned from the then stage. Which ever promise resolves first and calls it's then method, will first cancel all the promises in the array and then empty the array; only after that will invoke the provided callback function.

So now let's see the whole thing in action.

function makePromiseCancellable(p){
  Object.defineProperty(p,"__cancelled__", {        value: false,
                                                 writable: true,
                                               enumerable: false,
                                             configurable: false
                                           });
  Object.setPrototypeOf(p,makePromiseCancellable.prototype);
  return p;
}

makePromiseCancellable.prototype = Object.create(Promise.prototype);
makePromiseCancellable.prototype.then   = function(callback){
                                            return makePromiseCancellable(Promise.prototype.then.call(this,function(v){
                                                                                                             !this.__cancelled__ && callback(v);
                                                                                                           }.bind(this)));
                                          };
makePromiseCancellable.prototype.cancel = function(){
                                            this.__cancelled__ = true;
                                            return this;
                                          };

function getAsyncData(){
  var dur = ~~(Math.random()*9000+1001);
  return new Promise((v,x) => setTimeout(v.bind(this,"promise id " + pid++ + " resolved at " + dur + " msec"),dur));
}

function runner(fun,cb){
  var promises = [];
  return setInterval(_ => { var prom = makePromiseCancellable(fun());
                            promises.push(prom);
                            promises[promises.length-1].then(data => { promises.forEach(p => p.cancel());
                                                                       promises.length = 0;
                                                                       return cb(data);
                                                                     });
                          },1000);
}

var pid = 0,
    sid = runner(getAsyncData,v => console.log("received data:", v));
setTimeout(_=> clearInterval(sid),60001);

The runner function will run indefinitelly if you don't stop it. So at 60001msecs I clear it by a clearInterval(). During that period 60 promises will be received and only the first resolvers will invoke the provided callback by cancelling all the previous currently received promises, including the still unresolved ones, those received after the first resolving promise in our promises array. However since those later promises are expected to contain more fresh data, one might want to keep them uncancelled. Then I suppose the following small change in the code will do better in terms of refreshing the screen more frequently with the latest data.

function makePromiseCancellable(p){
  Object.defineProperty(p,"__cancelled__", {        value: false,
                                                 writable: true,
                                               enumerable: false,
                                             configurable: false
                                           });
  Object.setPrototypeOf(p,makePromiseCancellable.prototype);
  return p;
}

makePromiseCancellable.prototype = Object.create(Promise.prototype);
makePromiseCancellable.prototype.then   = function(callback){
                                            return makePromiseCancellable(Promise.prototype.then.call(this,function(v){
                                                                                                             !this.__cancelled__ && callback(v);
                                                                                                           }.bind(this)));
                                          };
makePromiseCancellable.prototype.cancel = function(){
                                            this.__cancelled__ = true;
                                            return this;
                                          };

function getAsyncData(){
  var dur = ~~(Math.random()*9000+1001);
  return new Promise((v,x) => setTimeout(v.bind(this,"promise id " + pid++ + " resolved at " + dur + " msec"),dur));
}

function runner(fun,cb){
  var promises = [];
  return setInterval(_ => { var prom = makePromiseCancellable(fun());
                            promises.push(prom);
                            promises[promises.length-1].then(data => { var prix = promises.indexOf(prom);
                                                                       promises.splice(0,prix)
                                                                               .forEach(p => p.cancel());
                                                                       return cb(data);
                                                                     });
                          },1000);
}

var pid = 0,
    sid = runner(getAsyncData,v => console.log("received data:", v));
setTimeout(_=> clearInterval(sid),60001);

There might be some flaws of course. I would like to hear your ideas.

Whereabouts answered 5/10, 2016 at 11:30 Comment(7)
Don't forget that then takes a second parameter and the first one might not always be a functionPitt
@Pitt yes you are right. I shall modify it once i have time. For the time being it will probably give an idea to the OP.Whereabouts
This is a comprehensive solution. I'll see about extracting it to some kind of service. Thank youAmbur
@Javier C Thanks I tried to be descriptive as much as possible. Though i have neglected the reject part for simplicity it's very similar. We have to rephrase our then implementation to accept a second argument as an onRejected callback for instance. On the other hand I don't think a cancellation should trigger a rejection though. Obviously it's us who is cancelling the promise and why should us be notified about it. Yet once there are cancellable promises we need an access to the promise status as well since it would be pointless to cancel an already resolved promise. That's another topic.Whereabouts
@Pitt "...and the first one might not always be a function". I thought it only accepts onfulfilled and optionally onRejected callbacks. I looked it up but couldn't find anything about the first argument not being a function. Could yo please elaborate this curious case..?Whereabouts
@Whereabouts It's optional, so it might be undefined/null (and really, every other non-function value should be treated like that).Pitt
Btw, if you don't want to "invent cancellable promises here" yourself you might want to have a look at github.com/bergus/creed/blob/cancellation/cancellation.mdPitt

© 2022 - 2024 — McMap. All rights reserved.