I'm in a situation where multiple parts of my app wants access to data. I would like the first caller to initiate a fetch from the server, whilst subsequent requests should wait for the data to be fetched. How do I do this with a Promise?
I've tried something along these lines, with no success:
var promise = null;
var fetchComplete = false;
var data = null;
function getData() {
if (fetchComplete) {
return new Promise(function(resolve, reject) {
resolve(data);
});
} else {
if (promise === null) {
promise = new Promise(function(resolve, reject) {
getDataFromServer(function(response) {
fetchComplete = true;
data = response;
});
});
}
else {
return promise;
}
}
};
You can use a closure like so:
function getDataPromise() {
return new Promise(function(resolve, reject) {
getDataFromServer(resolve);
});
}
var getDataCached = (function() {
var fetchPromise = null;
return function() {
if (!fetchPromise) {
// Fetch data here and populate fetchPromise with an actual promise.
fetchPromise = getDataPromise();
}
return fetchPromise;
};
})();
The first invokation will populate the fetchPromise closure variable, while the rest would simply return it.
Related
I am attempting to check if the current user is the owner of the record being edited and if so, I would allow the edit, if not I would prevent the edit. I have two functions that get me the current user and the user who created the record respectively. These are called separately since there are different dependencies. How can I combine these to a single function.
Based on a comments below and some debugging, I refactored my code to use promise.all. It is now functioning correctly and is a sample of how to code.
My code is the following:
<!-- Download SPServices from: //spservices.codeplex.com/ -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery.SPServices/2014.02/jquery.SPServices-2014.02.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
//don't exectute any jsom until sp.js file has loaded.
SP.SOD.executeFunc('sp.js', 'SP.ClientContext', StartProcess);
});
function StartProcess() {
var selectedValue = $("h3:contains('Request Phase')").closest('tr').find('select').val();
if (selectedValue == 'New') {
const DataFetch = Promise.all([
makeFirstCall().catch(error => {
return "error";
}),
makeSecondCall().catch(error => {
return "error";
})
]).then(resolvedData => {
// do something with resolved data
console.log('resolvedData='+resolvedData);
}).catch(error => {
console.error(error)
})
console.log('Succces');
}
}
const makeFirstCall = () => {
return new Promise(function(resolve, reject) {
//console.log('makeFirstCall');
// make your call
getCurrentUser().then(
function (currentUser) {
var loginId = currentUser.get_id();
//console.log('Current user ID='+loginId);
resolve(loginId);
}
);
});
}
const makeSecondCall = () => {
return new Promise(function(resolve, reject) {
console.log('makeSecondCall ');
// make your call
retrieveListItems().then(
function (requestUser) {
//var loginId = requestUser.get_id();
console.log('requestUser user ID='+requestUser);
resolve(requestUser);
}
);
});
}
function getCurrentUser()
{
//Declare your deferred object here
//console.log('getCurrentUser');
var deferred=$.Deferred();
var ctx = new SP.ClientContext.get_current();
this.website = ctx.get_web();
this.currentUser = website.get_currentUser();
ctx.load(currentUser);
ctx.executeQueryAsync(Function.createDelegate(this,
function () { deferred.resolve(currentUser); }),
Function.createDelegate(this,
function (sender, args) { deferred.reject(sender, args); })
);
//console.log('END-getCurrentUser');
//Return your Promise Object
return deferred.promise();
}
function retrieveListItems()
{
//Declare your deferred object here
console.log('retrieveListItems');
var deferred=$.Deferred();
var ctx = new SP.ClientContext.get_current();
// Get ID of the current item.
var currentItemID = window.location.href.toLowerCase();
currentItemID = currentItemID.substring(currentItemID.toLowerCase().indexOf("?id=") + 4,
currentItemID.toLowerCase().indexOf("&source="));
console.log('currentItemID='+currentItemID);
var oList = clientContext.get_web().get_lists().getByTitle('BSMRequests');
var camlQuery = new SP.CamlQuery();
camlQuery.set_viewXml('<View><Query><Where><Eq><FieldRef Name=\'ID\'/><Value Type=\'Text\'>'+ currentItemID+
'</Value></Eq></Where></Query></View>');
collListItem = oList.getItems(camlQuery);
ctx.load(collListItem);
//var loginId = "66";
//deferred.resolve(loginId);
ctx.executeQueryAsync(Function.createDelegate(this,function () {
var listItemEnumerator = collListItem.getEnumerator();
while (listItemEnumerator.moveNext()) {
var oListItem = listItemEnumerator.get_current();
console.log('Record id='+oListItem.get_item('ID'));
requestorID = oListItem.get_item('Requestor_x0020_Name').get_lookupId();
console.log('requestorID='+requestorID );
}
deferred.resolve(requestorID);
}),
Function.createDelegate(this,function (sender, args) { deferred.reject(sender, args); })
);
console.log('END-retrieveListItems');
//Return your Promise Object
return deferred.promise();
}
</script>
My code will return the two ids and then one can do what they want with those. Those IDs are in an array so comparing them would do the trick.
console.log('Userid: '+resolvedData[0]+'=? Userid: '+resolvedData[1]);
Hi you can make both calls into promises and use promise.all to get the response. That way you wait for both results.
const DataFetch = () => {Promise.all([
makeFirstCall().catch(error => {
return "error";
}),
makeSecondCall().catch(error => {
return "error";
})
]).then(resolvedData => {
// do something with resolved data
}).catch(error => {
console.error(error)
})}
To make the promise function, you can do this:
const makeFirstCall = () => {
return new Promise(function(resolve, reject) {
// make your call
reject(new Error(`error message`)); // if error
resolve(dataFromCall);
});
}
I have the following discovery code using the mdns-js package.
in ./lib/deviceDiscovery.js:
var mdns = require('mdns-js');
const browsers = new Map();
const INFINITE = -1;
function createABrowser(theServiceType, timeout) {
if (browsers.has(theServiceType)) {
return;
}
return new Promise(function(resolve, reject) {
var browser = mdns.createBrowser(theServiceType);
browser.on('ready', function() {
browsers.set(theServiceType, browser);
resolve(browser);
});
if (timeout != INFINITE) {
setTimeout(function onTimeout() {
try {
browser.stop();
browsers.delete(browser.serviceType);
} finally {
reject('browser ' + browser.toString() + ' timed out.');
}
}, timeout);
}
});
}
module.exports.startService = function(services, timeout) {
timeout = timeout || INFINITE;
promises = [];
services.forEach(function(service) {
promises.push(createABrowser(service, timeout));
});
return Promise.all(promises);
}
module.exports.stopService = function() {
browsers.values().forEach(function(browser) {
browser.stop();
});
browsers.clear();
}
module.exports.getDevices = function() {
if (browsers.size == 0) {
reject('service was stopped');
} else {
const promises = [];
for (let browser of browsers.values()) {
promises.push(new Promise(function(resolve, reject) {
try {
browser.discover();
browser.on('update', function(data) {
mfps = new Set();
const theAddresses = data.addresses;
theAddresses.forEach(function(element) {
mfps.add(element);
});
resolve(mfps);
});
} catch(err) {
reject(err);
}
}));
};
return Promise.all(promises).then(function(values) {
return new Set(values);
}, function(reason) {
return reason;
});
}
}
and use it in another file like this:
const DeviceDiscoveryService = require('./lib/deviceDiscovery');
var co = require('co');
co(function *service() {
yield DeviceDiscoveryService.startService([internetPrinter, pdlPrinter, unixPrinter], TIMEOUT);
yield DeviceDiscoveryService.getDevices();
}).catch(onerror);
function onerror(err) {
// log any uncaught errors
}
The problem is that the second yield hangs; it seems that the promise returned by getDevices function isn't resolved indefinitely, although I see that the individual promises are resolved.
startService uses a similar Promise.all(...) but it works ok.
Another related question is about the mdns-js: it seems that for each (input) service, the browser receives multiple updates.
But I resolve the promise for each browser after the first update event... do I need to wait for multiple updates and how?
Any hints will be greatly appreciated! Thanks.
I believe that you share update be returning a promises from createABrowser at ALL times (instead of returning undefined if the service already exists). Without returning a promise, I think Promise.all() won't resolve.
Instead, create a promise at the top and resolve if it the service exists already, and return THAT promise.
For the getDevices() call, you're running a reject without returning a promise there as well. Would this work?
module.exports.getDevices = function() {
if (browsers.size == 0) {
// Create a new promise, return it, and immediately reject
return new Promise(function(resolve, reject) { reject('service was stopped') };
// reject('service was stopped'); <- There wasn't a promise here
} else {
const promises = [];
for (let browser of browsers.values()) {
promises.push(new Promise(function(resolve, reject) {
try {
browser.discover();
browser.on('update', function(data) {
mfps = new Set();
const theAddresses = data.addresses;
theAddresses.forEach(function(element) {
mfps.add(element);
});
resolve(mfps);
});
} catch(err) {
reject(err);
}
}));
};
return Promise.all(promises).then(function(values) {
return new Set(values);
}, function(reason) {
return reason;
});
}
}
So for example, lets say I have this code:
var cmd = require('node-cmd')
function getStuff() {
return new Promise((resolve, reject) => {
var dataNStuff;
cmd.get('brew --version', data => {
dataNStuff += data;
})
cmd.get('yarn global ls', data => {
dataNStuff += data;
})
resolve(dataNStuff)
})
}
In this case cmd.get() is async, so I don't know when the data is coming in. I want to be able to have both calls already have the data come in before I resolve(dataNStuff), is this even possible with a Promise, and no I do not want to use a callback in this scenario. Is there a much simpler or faster way of doing the exact same thing?
Using Promises for the solution, use Promise.all, and "promisified" version of cmd.get
var cmd = require('node-cmd');
var cmdPromise = arg => new Promise((resolve, reject) => cmd.get(arg, resolve));
function getStuff() {
return Promise.all([cmdPromise('brew --version'), cmdPromise('yarn global ls')])
.then(results => results.join(''));
}
to "explain" cmdPromise in case that compact version isn't readable, it's basically this:
var cmdPromise = function cmdPromise(arg) {
return new Promise((resolve, reject) => {
cmd.get(arg, data => resolve(data));
});
};
Here's a straightforward solution involving Promises.
function getStuff() {
var dataNStuff = '';
var promiseOne = new Promise(function(resolve, reject) {
cmd.get('brew --version', data => {
dataNStuff += data;
resolve();
});
});
var promiseTwo = new Promise(function(resolve, reject) {
cmd.get('yarn global ls', data => {
dataNStuff += data;
resolve();
});
});
return Promise.all([promiseOne, promiseTwo]).then(function() {
return dataNStuff;
});
}
I assume that cmd.get will execute one after the other. If this assumption is incorrect, then there is no guarantee of the order of the strings. (ie It may display brew before yarn or it may display yarn before brew.)
ES2017 answer: removing the .then() in favor of = via async/await (as the comment from #stephen-bugs-kamenar mentions.
var log = console.log;
var getBanana = function() {
return new Promise(function(resolve, reject) {
setTimeout(function(){
resolve('banana')
}, 2000);
});
}
var getGrape = function() {
return new Promise(function(resolve, reject) {
setTimeout(function(){
resolve('grape')
}, 3000);
});
}
var start = async function(){
var result = await Promise.all([getBanana(), getGrape(), 'apple'])
console.log('All done', result)
}
start();
I'm querying a REST-API to get all groups. Those groups come in batches of 50. I would like to collect all of them before continuing to process them.
Up until now I relied on callbacks but I'd like to use promises to chain the retrieval of all groups and then process the result-array further.
I just don't quite get how to replace the recursive functional call using promises.
How would I use A+ promises to escape the callback hell I create with this code?
function addToGroups() {
var results = []
collectGroups(0)
function collectGroups(offset){
//async API call
sc.get('/tracks/'+ CURRENT_TRACK_ID +'/groups?limit=50&offset=' + offset , OAUTH_TOKEN, function(error, data){
if (data.length > 0){
results.push(data)
// keep requesting new groups
collectGroups(offset + 50)
}
// finished
else {
//finish promise
}
})
}
}
Using standard promises, wrap all of your existing code as shown here:
function addToGroups() {
return new Promise(function(resolve, reject) {
... // your code, mostly as above
});
}
Within your code, call resolve(data) when you're finished, or reject() if for some reason the chain of calls fails.
To make the whole thing more "promise like", first make a function collectGroups return a promise:
function promiseGet(url) {
return new Promise(function(resolve, reject) {
sc.get(url, function(error, data) {
if (error) {
reject(error);
} else {
resolve(data);
}
});
}
}
// NB: promisify-node can do the above for you
function collectGroups(offset, stride) {
return promiseGet('/tracks/'+ CURRENT_TRACK_ID +'/groups?limit=' + stride + '&offset=' + offset , OAUTH_TOKEN);
}
and then use this Promise in your code:
function addToGroups() {
var results = [], stride = 50;
return new Promise(function(resolve, reject) {
(function loop(offset) {
collectGroups(offset, stride).then(function(data) {
if (data.length) {
results.push(data);
loop(offset + stride);
} else {
resolve(data);
}
}).catch(reject);
)(0);
});
}
This could work. I am using https://github.com/kriskowal/q promises.
var Q = require('q');
function addToGroups() {
var results = []
//offsets hardcoded for example
var a = [0, 51, 101];
var promises = [], results;
a.forEach(function(offset){
promises.push(collectGroups(offset));
})
Q.allSettled(promises).then(function(){
promises.forEach(function(promise, index){
if(promise.state === 'fulfilled') {
/* you can use results.concatenate if you know promise.value (data returned by the api)
is an array */
//you also could check offset.length > 0 (as per your code)
results.concatenate(promise.value);
/*
... do your thing with results ...
*/
}
else {
console.log('offset',index, 'failed', promise.reason);
}
});
});
}
function collectGroups(offset){
var def = Q.defer();
//async API call
sc.get('/tracks/'+ CURRENT_TRACK_ID +'/groups?limit=50&offset=' + offset , OAUTH_TOKEN, function(error, data){
if(!err) {
def.resolve(data);
}
else {
def.reject(err);
}
});
return def.promise;
}
Let me know if it works.
Here's complete example, using spex.sequence:
var spex = require("spex")(Promise);
function source(index) {
return new Promise(function (resolve) {
sc.get('/tracks/' + CURRENT_TRACK_ID + '/groups?limit=50&offset=' + index * 50, OAUTH_TOKEN, function (error, data) {
resolve(data.length ? data : undefined);
});
});
}
spex.sequence(source, {track: true})
.then(function (data) {
// data = all the pages returned by the sequence;
});
I don't think it can get simpler than this ;)
I am trying to reuse the the data returned from promise here. But, the problem is, after the first call to checkPromise function, it immediately calls the second function, and the promise for the first function is not fulfilled, so it never returns any data, and hence it never enters the if clause. How do I reuse a promise?
var Promise = require('bluebird');
var request = Promise.promisify(require("request"));
var url = 'http://www.google.com';
var obj = new Object;
function apiCall(url) {
return new Promise(function (resolve, reject) {
request(url).spread(function(response, body) {
return resolve(body);
}).catch(function(err) {
console.error(err);
return reject(err);
});
});
}
function checkPromise(url) {
if(obj.hasOwnProperty(url)) {
var rp = obj[url];
//do something
}
else {
apiCall(url).then(function(result) {
obj[url] = result;
//do something
});
}
}
checkPromise(url);
checkPromise(url);
You likely have a timing issue. Your apiCall() function is asynchronous. That means it finishes sometime later. As such, each time you call checkPromise(), all you're doing is starting a request and it finishes sometime later. So, you call it the first time and it starts a request (that has not finished yet). Then, your next call to checkPromise() gets called and it does it's if check before the first call has completed. Thus, it finds nothing in the cache yet.
Your code is running two requests in parallel, not one after the other.
If you actually want to wait until the first request is done before executing the second one, then you will have to actually structure your code to do that. You would need to make checkPromise() return a promise itself so code using it could known when it was actually done in order to execute something after it was done.
FYI, I don't see anything in your code that is actually related to reusing promises (which is something you cannot do because they are one-shot objects).
Here's one possible implementation:
var Promise = require('bluebird');
var request = Promise.promisify(require("request"));
var url = 'http://www.google.com';
var obj = {};
function apiCall(url) {
return request(url).spread(function(response, body) {
return body;
});
}
function checkPromise(url) {
if(obj.hasOwnProperty(url)) {
var rp = obj[url];
//do something
return Promise.resolve(rp);
}
else {
return apiCall(url).then(function(result) {
obj[url] = result;
//do something
return result;
});
}
}
checkPromise(url).then(function() {
checkPromise(url);
});
Significant changes:
Return the promise returned by request() rather than create yet another one.
Change checkPromise() so it always returns a promise whether the value is found in the cache or not so calling code can always work consistently.
Sequence the two checkPromise() calls so the first can finish before the second is executed.
A very different approach would be to actually wait on the cache if a result you are interested in is already being loaded. That could be done like this:
var Promise = require('bluebird');
var request = Promise.promisify(require("request"));
var url = 'http://www.google.com';
var obj = {};
function apiCall(url) {
return request(url).spread(function(response, body) {
return body;
});
}
function checkPromise(url) {
if(obj.hasOwnProperty(url)) {
// If it's a promise object in the cache, then loading
// If it's a value, then the value is already available
// Either way, we wrap it in a promise and return that
return Promise.resolve(obj[url]);
} else {
var p = apiCall(url).then(function(result) {
obj[url] = result;
//do something
return result;
});
obj[url] = p;
return p;
}
}
checkPromise(url).then(function(result) {
// use result
});
checkPromise(url).then(function(result) {
// use result
});
few problems with your code, first in apiCall, you are doing a promise ant-pattern( no need for that new promise), second your checkPromise is doing a sync operation, so it must either return a promise or have a callback argument, so you code can be changed into:
var Promise = require('bluebird');
var request = Promise.promisify(require("request"));
var url = 'http://www.google.com';
var obj = new Object;
function apiCall(url) {
return request(url).spread(function(response, body) {
return body;
}).catch(function(err) {
console.error(err);
throw err;
});
}
function checkPromise(url) {
var promise = Promise.resolve();
if(obj.hasOwnProperty(url)) {
var rp = obj[url];
//do something
}
else {
return apiCall(url).then(function(result) {
obj[url] = result;
//do something
});
}
return promise;
}
checkPromise(url).then(function(){
return checkPromise(url);
});
Given the way you are globally storing the result in 'obj[url]', it'd probably be easiest to do
function checkPromise(url) {
if (!obj[url]) obj[url] = apiCall(url);
obj[url].then(function(result) {
//do something
});
}
to basically make the request, if it hasn't already started, then attach a listener to the promise for when the result has loaded.
Here is the simplest example of how to prevent multiple API calls if there are multiple similar request for something (cache check for example)
var _cache = {
state: 0,
result: undefined,
getData: function(){
log('state: ' + this.state);
if(this.state === 0 ){ // not started
this.state = 1; // pending
this.promise = new Promise(function(resolve, reject) {
return (apiCall().then(data => { _cache.result = data; _cache.state = 2; resolve(_cache.result) }));
})
return this.promise;
}
else if(this.state === 1){ // pending
return this.promise;
}
else if(this.state === 2){// resolved
return Promise.resolve(this.result);
}
},
};
Simulating api call
function apiCall(){
return new Promise(function(resolve, reject) {
log('in promise')
setTimeout(() => {
log('promise resolving')
resolve(1);
}, 1000);
})
}
Making simultaneous requests.
_cache.getData().then(result => { log('first call outer: ' + result);
_cache.getData().then(result => { log('first call inner: ' + result); });
});
_cache.getData().then(result => { log('second call outer: ' + result);
_cache.getData().then(result => { log('second call inner: ' + result); });
});
Only one API call is maden. All others will wait for completion or use the resolved result if it already completed.