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.
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;
}
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);
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With