Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaScript Synchronized Promise Return

I have a JavaScript method that promises an object. That object should be fetched the first time, but thereafter, a cached instance should be returned. In order to simulate the retrieval, we'll put a delay in here.

var Promise = require('bluebird');
var retrievedObject = null;
var retrievalCount = 0;

var retrieveObject = Promise.coroutine(
function *retrieveObject(){
  if (retrievedObject) {
    return retrievedObject;
  }

  yield Promise.delay(1000); // wait one second
  retrievedObject = ++retrievalCount;

  return retrievedObject;
});

In this instance, for simplicity's sake, we just wait for one second and then return an incremented count. If the caching works properly, the count should be incremented only once, whereas all the times afterwards, it should stay at the same value, which is 1. This should serve our needs as a testing mechanism of whether or not the incrementing code has been reached or not.

Now, I also have a function that's supposed to test the object's retrieval, defined thus:

var testRetrievals = Promise.coroutine(
function *testRetrievals() {
  var count = yield retrieveObject();
  console.log(count);
});

When calling that function multiple times, (like this):

testRetrievals();
testRetrievals();
testRetrievals();
testRetrievals();
testRetrievals();

The console log should read:

1 1 1 1 1

What it really reads, however, is

1 2 3 4 5

The reason for that is obvious. The retrieveObject method is called before the first call's promise yields because it takes less than one second for all the calls to start. However, I'm looking for a way to somehow synchronize the retrieveObject method such that after the first call, it wouldn't go into the retrieval procedure (the delay in our case), but wait for the first call to complete. What would be the most JavaScript/Node-idiomatic way of doing that? I'm thinking of introducing semaphore-like structures, but I'm worried that that would break Node's non-waiting behavior.

like image 846
arik Avatar asked Jul 19 '26 09:07

arik


2 Answers

I'm thinking of introducing semaphore-like structures

Well yes, you could do that. The main thing we need to do is to create the slot in our cache right when the method is called, not after awaiting the retrieval.

So next to the result value to store we would need a pending flag that tracks whether we already begun retrieving, and a way to notify listeners that wait for change of the flag while it's not yet settled
Sounds familiar? Right, that's a promise, it has exactly these semaphores already built in.

You just store the promise itself in the cache:

var Promise = require('bluebird');
var retrievedPromise = null;
var retrievalCount = 0;

var retrieveObject = Promise.coroutine(function* retrieveObject() {
    if (retrievedPromise) {
        return yield retrievedPromise;
    }

    retrievedPromise = Promise.delay(++retrievalCount, 1000); // wait one second
    var retrievedObject = yield retrievedPromise;
    return retrievedObject;
});

In fact you don't even need a coroutine with yields for that:

…
var retrievedPromise = null;
function retrieveObject() {
    if (!retrievedPromise) 
        retrievedPromise = Promise.delay(++retrievalCount, 1000); // wait one second
    return retrievedPromise;
}
like image 121
Bergi Avatar answered Jul 21 '26 23:07

Bergi


Apparently, promises can only resolve or reject once. That means that if we replace the promise-generating function retrieveObject with a promise, which we do by simply adding a pair of parentheses at the end, like this:

var retrieveObject = Promise.coroutine(
function *retrieveObject(){
  if (retrievedObject) {
    return retrievedObject;
  }

  yield Promise.delay(1000); // wait one second
  retrievedObject = ++retrievalCount;

  return retrievedObject;
})();

retrieveObject will no longer create new promises when called, but be a promise itself. After it resolves for the first time, all the subsequent "resolutions" will in fact be instantaneous responses of the value that has been cached under the hood. Which means that we can completely discard the memoization we do ourselves:

var retrieveObject = Promise.coroutine(
function *retrieveObject(){
  yield Promise.delay(1000); // wait one second
  retrievedObject = ++retrievalCount;

  return retrievedObject;
})();

That means that now, the testRetrievals method has to lose the parentheses when yielding retrieveObject:

var testRetrievals = Promise.coroutine(
function *testRetrievals() {
  var count = yield retrieveObject;
  console.log(count);
});

And voilá, the console output looks like this:

1 1 1 1 1

However, as user Bergi rightly pointed out, that would result in the promise always being called (once), even if it was never required. So one workaround would be to make the retrieveObject function be a promise-generating one again, like this:

var retrieveObject = Promise.coroutine(
function *retrieveObject(){
  yield Promise.delay(1000); // wait one second
  retrievedObject = ++retrievalCount;

  return retrievedObject;
});

And what we do now is, in the function that relies on retrieveObject, testRetrievals, we memoize the promise during the first run:

var retrieveObjectPromise = null;
var testRetrievals = Promise.coroutine(
function *testRetrievals() {
  if (!retrieveObjectPromise){
    retrieveObjectPromise = retrieveObject();
  }
  var count = yield retrieveObjectPromise;
  console.log(count);
});
like image 33
arik Avatar answered Jul 21 '26 22:07

arik



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!