9

Can anyone recommend a pattern for instantly retrieving data from a function that returns a Promise?

My (simplified) example is an AJAX preloader:

loadPage("index.html").then(displayPage);

If this is downloading a large page, I want to be able to check what's happening and perhaps cancel the process with an XHR abort() at a later stage.

My loadPage function used to (before Promises) return an id that let me do this later:

var loadPageId = loadPage("index.html",displayPage);
...
doSomething(loadPageId);
cancelLoadPage(loadPageId);

In my new Promise based version, I'd imagine that cancelLoadPage() would reject() the original loadPage() Promise.

I've considered a few options all of which I don't like. Is there a generally accepted method to achieve this?

Jamie G
  • 1,653
  • 3
  • 20
  • 43
  • For reference, methods I considered include: returning an object that contains the promise along with other data; saving the data in the Promise object; having a getLastId() method or (equivalent to this) having it's own class so requiring var obj = new PageLoader(); obj.loadPage(); var id = obj,getId(); – Jamie G Aug 25 '16 at 08:58
  • What kind of data are you expecting to receive from the function? – Omri Aharon Sep 11 '16 at 14:07
  • @Omri I don't think it should make a different what kind of data, just something that has been created/modified by the first synchronous part of my promise function. – Jamie G Sep 11 '16 at 14:11
  • So why isn't returning an object wrapping the promise working for you? I don't really get what it is you're trying to achieve... you can cancel promises. – Omri Aharon Sep 11 '16 at 14:12
  • @OmriAharon: Consider an object that returns a promise. 20 seconds later the promise's `then()` still haven't run. Now you think it's too long and assume the server has failed somehow because even if you get the data in 3 hours time that's too long for your application. Now, cancel the promise.. somehow. Regular promises can't do this. – slebetman Sep 11 '16 at 14:19
  • @slebetman moreover, it's not *just* about cancelling the promise, it's about being able to know and access stuff that the function has done so far. – Jamie G Sep 11 '16 at 14:21
  • @Omri your suggest is one option, feel free to suggest it as an answer! I'm looking for any suggestions that other people find work for them. – Jamie G Sep 11 '16 at 14:22
  • A new technique I've tried today is to pass an object as a parameter that gets populated with stuff you might need later. Eg. var instantReturns = {}; loadPage("index.html", instantReturns).then(displayPage); console.log(instantReturns); //Shows stuff done by the promise so far. I think this is working ok, but it doesn't feel very clean! – Jamie G Sep 11 '16 at 14:31
  • "*I've considered a few options*" - can you list them please? – Bergi Sep 13 '16 at 11:59
  • Regarding the scenario in the bounty description (you really should've [edit]ed your question), why not simply return a composite object that contains both the synchronous results as well as a promise for the asynchronous results? – Bergi Sep 13 '16 at 12:01
  • Sorry Bergi, only just saw your comment. See the first comment here where I list my original ideas, and then my comment on 11 September. Your suggestion is certainly valid. I was expecting a few simple answers of what people do themselves - I think my bounty might have lead to higher expectations and v. complex answers! – Jamie G Sep 16 '16 at 16:19
  • If you just need to check the progress of a XHR, you can use the `progress` event: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress You'll only be able to get the percentage completed (if available), but this might be enough info in order for you to abort the request. – Daniel T. Feb 14 '17 at 03:25

6 Answers6

4

Okay, let's address your bounty note first.

[Hopefully I'll be able to grant the points to someone who says more than "Don't use promises"... ]

Sorry, but the answer here is: "Don't use promises". ES6 Promises have three possible states (to you as a user): Pending, Resolved and Rejected (names may be slightly off).

There is no way for you to see "inside" of a promise to see what has been done and what hasn't - at least not with native ES6 promises. There was some limited work (in other frameworks) done on promise notifications, but those did not make it into the ES6 specification, so it would be unwise of you to use this even if you found an implementation for it.

A promise is meant to represent an asynchronous operation at some point in the future; standalone, it isn't fit for this purpose. What you want is probably more akin to an event publisher - and even that is asynchronous, not synchronous.

There is no safe way for you to synchronously get some value out of an asynchronous call, especially not in JavaScript. One of the main reasons for this is that a good API will, if it can be asynchronous, will always be asynchronous.

Consider the following example:

const promiseValue = Promise.resolve(5)
promiseValue.then((value) => console.log(value))
console.log('test')

Now, let's assume that this promise (because we know the value ahead of time) is resolved synchronously. What do you expect to see? You'd expect to see:

> 5
> test

However, what actually happens is this:

> test
> 5

This is because even though Promise.resolve() is a synchronous call that resolves an already-resolved Promise, then() will always be asynchronous; this is one of the guarantees of the specification and it is a very good guarantee because it makes code a lot easier to reason about - just imagine what would happen if you tried to mix synchronous and asynchronous promises.

This applies to all asynchronous calls, by the way: any action in JavaScript that could potentially be asynchronous will be asynchronous. As a result, there is no way for you do any kind of synchronous introspection in any API that JavaScript provides.

That's not to say you couldn't make some kind of wrapper around a request object, like this:

function makeRequest(url) {
  const requestObject = new XMLHttpRequest()
  const result = {

  }

  result.done = new Promise((resolve, reject) => {
    requestObject.onreadystatechange = function() {
      ..
    }
  })
  requestObject.open(url)
  requestObject.send()
  return requestObject
}

But this gets very messy, very quickly, and you still need to use some kind of asynchronous callback for this to work. This all falls down when you try and use Fetch. Also note that Promise cancellation is not currently a part of the spec. See here for more info on that particular bit.

TL:DR: synchronous introspection is not possible on any asynchronous operation in JavaScript and a Promise is not the way to go if you were to even attempt it. There is no way for you to synchronously display information about a request that is on-going, for example. In other languages, attempting to do this would require either blocking or a race condition.

Sᴀᴍ Onᴇᴌᴀ
  • 8,218
  • 8
  • 36
  • 58
Dan
  • 10,282
  • 2
  • 37
  • 64
  • 1
    upvote for "don't use promises for this". I recommend using an async stream - which is already built into the new native XHR API (fetch) - `fetch` returns a promise that resolves _when the headers are delivered_ at which point you can read the response as a stream and parse it - processing it part by part - you can also abort it if not interested in more data. This is all in the specification. Promises are not a one size fit all. – Benjamin Gruenbaum Sep 13 '16 at 12:38
  • Thanks for your time Dan, this is a holding comment until I have time to properly read, consider and digest your post. Thanks – Jamie G Sep 13 '16 at 13:11
  • @BenjaminGruenbaum do you have some information on that async stream stuff? I did check the Fetch docs when writing this answer but couldn't find any information about Fetch actually being an async stream. – Dan Sep 14 '16 at 06:16
  • @DanPantry See [How can I append DOM elements as they stream in from the network?](http://stackoverflow.com/questions/38413400/how-can-i-append-dom-elements-as-they-stream-in-from-the-network/). Parsing `html` from a stream is not trivial; though not necessarily impossible. – guest271314 Sep 16 '16 at 01:54
  • @DanPantry - I'd like to check: my premise is that I'm writing a function that returns a promise. I'm not sure if that is what you are also talking about, or if you are discussing the Promise itself which is slightly different. I understand that the Promise is async, but I'm doing stuff before the promise object is even created and that's what I'm trying to get my hands on. I want to give you credit for an excellently written and very helpful answer, but I'm not sure yet if it does really address my question, or perhaps I'm not getting it 100%! – Jamie G Sep 16 '16 at 16:04
1

Well. If using angular you can make use of the timeout parameter used by the $http service if you need to cancel and ongoing HTTP request.

Example in typescript:

interface ReturnObject {
  cancelPromise: ng.IPromise;
  httpPromise: ng.IHttpPromise;
}

@Service("moduleName", "aService")
class AService() {

  constructor(private $http: ng.IHttpService
              private $q: ng.IQService) { ; }

  doSomethingAsynch(): ReturnObject {
    var cancelPromise = this.$q.defer();
    var httpPromise = this.$http.get("/blah", { timeout: cancelPromise.promise });
    return { cancelPromise: cancelPromise, httpPromise: httpPromise };
  }
}

@Controller("moduleName", "aController")
class AController {

  constructor(aService: AService) {
    var o = aService.doSomethingAsynch();

    var timeout = setTimeout(() => {
      o.cancelPromise.resolve();
    }, 30 * 1000);

    o.httpPromise.then((response) => {
      clearTimeout(timeout);
      // do code
    }, (errorResponse) => {
      // do code
    });
  }
}

Since this approach already returns an object with two promises the stretch to include any synchronous operation return data in that object is not far.

If you can describe what type of data you would want to return synchronously from such a method it would help to identify a pattern. Why can it not be another method that is called prior to or during your asynchronous operation?

Hampus
  • 2,769
  • 1
  • 22
  • 38
1

You can kinda do this, but AFAIK it will require hacky workarounds. Note that exporting the resolve and reject methods is generally considered a promise anti-pattern (i.e. sign you shouldn't be using promises). See the bottom for something using setTimeout that may give you what you want without workarounds.

let xhrRequest = (path, data, method, success, fail) => {
  const xhr = new XMLHttpRequest();

  // could alternately be structured as polymorphic fns, YMMV
  switch (method) {
    case 'GET':
      xhr.open('GET', path);
      xhr.onload = () => {
          if (xhr.status < 400 && xhr.status >= 200) {
            success(xhr.responseText);
            return null;
          } else {
            fail(new Error(`Server responded with a status of ${xhr.status}`));
            return null;
          }
        };
        xhr.onerror = () => {
          fail(networkError);
          return null;
        }
        xhr.send();
        return null;
      }

      return xhr;

    case 'POST':
      // etc.
      return xhr;

    // and so on...
};

// can work with any function that can take success and fail callbacks
class CancellablePromise {
  constructor (fn, ...params) {
    this.promise = new Promise((res, rej) => {
      this.resolve = res;
      this.reject = rej;
      fn(...params, this.resolve, this.reject);
      return null;
    });
  }
};

let p = new CancellablePromise(xhrRequest, 'index.html', null, 'GET');

p.promise.then(loadPage).catch(handleError);

// times out after 2 seconds
setTimeout(() => { p.reject(new Error('timeout')) }, 2000);

// for an alternative version that simply tells the user when things 
// are taking longer than expected, NOTE this can be done with vanilla 
// promises:

let timeoutHandle = setTimeout(() => {
  // don't use alert for real, but you get the idea
  alert('Sorry its taking so long to load the page.');
}, 2000);

p.promise.then(() => clearTimeout(timeoutHandle));
Jared Smith
  • 19,721
  • 5
  • 45
  • 83
1

Promises are beautiful. I don't think there is any reason that you can not handle this with promises. There are three ways that i can think of.

  1. The simplest way to handle this is within the executer. If you would like to cancel the promise (like for instance because of timeout) you just define a timeout flag in the executer and turn it on with a setTimeout(_ => timeout = true, 5000) instruction and resolve or reject only if timeout is false. ie (!timeout && resolve(res) or !timeout && reject(err)) This way your promise indefinitely remains unresolved in case of a timeout and your onfulfillment and onreject functions at the then stage never gets called.
  2. The second is very similar to the first but instead of keeping a flag you just invoke reject at the timeout with proper error description. And handle the rest at the then or catch stage.
  3. However if you would like to carry the id of your asych operation to the sync world then you can also do it as follows;

In this case you have to promisify the async function yourself. Lets take an example. We have an async function to return the double of a number. This is the function

function doubleAsync(data,cb){
  setTimeout(_ => cb(false, data*2),1000);
}

We would like to use promises. So normally we need a promisifier function which will take our async function and return another function which when run, takes our data and returns a promise. Right..? So here is the promisifier function;

function promisify(fun){
  return (data) => new Promise((resolve,reject) => fun(data, (err,res) => err ? reject(err) : resolve(res)));
}

Lets se how they work together;

function promisify(fun){
  return (data) => new Promise((resolve,reject) => fun(data, (err,res) => err ? reject(err) : resolve(res)));
}

function doubleAsync(data,cb){
  setTimeout(_ => cb(false, data*2),1000);
}

var doubleWithPromise = promisify(doubleAsync);
doubleWithPromise(100).then(v => console.log("The asynchronously obtained result is: " + v));

So now you see our doubleWithPromise(data) function returns a promise and we chain a then stage to it and access the returned value.

But what you need is not only a promise but also the id of your asynch function. This is very simple. Your promisified function should return an object with two properties; a promise and an id. Lets see...

This time our async function will return a result randomly in 0-5 secs. We will obtain it's result.id synchronously along with the result.promise and use this id to cancel the promise if it fails to resolve within 2.5 secs. Any figure on console log Resolves in 2501 msecs or above will result nothing to happen and the promise is practically canceled.

function promisify(fun){
  return function(data){
           var result = {id:null, promise:null};       // template return object
           result.promise = new Promise((resolve,reject) => result.id = fun(data, (err,res) => err ? reject(err) : resolve(res)));
           return result;
         };
}

function doubleAsync(data,cb){
  var dur = ~~(Math.random()*5000);                    // return the double of the data within 0-5 seconds.
  console.log("Resolve in " + dur + " msecs");
  return setTimeout(_ => cb(false, data*2),dur);
}

var doubleWithPromise = promisify(doubleAsync),
       promiseDataSet = doubleWithPromise(100);

setTimeout(_ => clearTimeout(promiseDataSet.id),2500); // give 2.5 seconds to the promise to resolve or cancel it.
promiseDataSet.promise
              .then(v => console.log("The asynchronously obtained result is: " + v));
Redu
  • 25,060
  • 6
  • 56
  • 76
  • This is a great example of how promises can work however the issue with OP's question is that OP wants to - from my point of view - return part of the result of an asynchronous action, but synchronous. My post focuses on the point that this is not possible because of the nature of asynchronous APIs. Nonetheless this is a good answer, so +1 – Dan Sep 16 '16 at 06:03
  • Hi @Redu, thanks for all your efforts! I think this is basically what I have, (I did actually consider using set timeout as my example in my original post). I haven't specified my "promisify" function separately in my case, but I guess I could have done. I think important bit of your answer for me is that you advocate returning an object that contains the promise as well as any other data. – Jamie G Sep 16 '16 at 16:42
  • @Redu, out of interest, what do you think about my idea that I put in a comment here http://stackoverflow.com/questions/39140670/js-promise-instantly-retrieve-some-data-from-a-function-that-returns-a-promise#comment66196605_39140670 compared to this? – Jamie G Sep 16 '16 at 16:43
  • @Jamie G Yes that's exactly what i am saying. Whatever data is available/given to you at the time you make your asynchronous call (such as the async function id) you can return it (along with the promise object) by your promise executer and handle it in the synchronous time-line. – Redu Sep 16 '16 at 16:46
  • @Jamie G as per your comment that's pretty valid, provided that the promisified function is able return something other than the promise itself and i doubt it for most of the cases. It's probably best to promisify the async function with a promisifier, something like the one i have given in the above code. – Redu Sep 16 '16 at 17:06
1

You can use fetch(), Response.body.getReader(), where when .read() is called returns a ReadableStream having a cancel method, which returns a Promise upon cancelling read of the stream.

// 58977 bytes of text, 59175 total bytes
var url = "https://gist.githubusercontent.com/anonymous/"
          + "2250b78a2ddc80a4de817bbf414b1704/raw/"
          + "4dc10dacc26045f5c48f6d74440213584202f2d2/lorem.txt";
var n = 10000;
var clicked = false;
var button = document.querySelector("button");
button.addEventListener("click", () => {clicked = true});

fetch(url)
.then(response => response.body.getReader())
.then(reader => {
  var len = 0;
  reader.read().then(function processData(result) {
    if (result.done) {
      // do stuff when `reader` is `closed`
      return reader.closed.then(function() {
        return "stream complete"
      });
    };
    if (!clicked) {
      len += result.value.byteLength;
    }
    // cancel stream if `button` clicked or 
    // to bytes processed is greater than 10000
    if (clicked || len > n) {
      return reader.cancel().then(function() {
        return "read aborted at " + len + " bytes"
      })
    }
    console.log("len:", len, "result value:", result.value);
    return reader.read().then(processData)
  })
  .then(function(msg) {
    alert(msg)
  })
  .catch(function(err) {
    console.log("err", err)
  })
});
<button>click to abort stream</button>
guest271314
  • 1
  • 15
  • 104
  • 177
0

The method I am currently using is as follows:

var optionalReturnsObject = {};
functionThatReturnsPromise(dataToSend, optionalReturnsObject ).then(doStuffOnAsyncComplete);
console.log("Some instant data has been returned here:", optionalReturnsObject ); 

For me, the advantage of this is that another member of my team can use this in a simple way:

functionThatReturnsPromise(data).then(...);

And not need to worry about the returns object. An advanced user can see from the definitions what is going on.

Jamie G
  • 1,653
  • 3
  • 20
  • 43
  • BTW: I'm not posting this as a correct answer, I'm still very interested in other opinions on this question - as of 20/09/16 I'm not satisfied that I have an answer / enough answers! – Jamie G Sep 20 '16 at 07:57