Tracking state of a chain of promises - javascript

I'm currently trying to track the progress of a chain of native es6 promises, and am wondering what the 'correct' way to go about this is.
I've simplified the actual code to thie following example, which is a basic set of chained promises (in reality, the promise chain is longer, and the session status value changes in more places depending on progress through the chain):
let sessions = {}
const asyncFunc = () => {
// Get a new id for state tracking
let session_id = getID()
sessions.session_id = 'PENDING'
// Fetch the first url
let result = api.get(url)
.then(res => {
// Return the 'url' property of the fetched data
return res.url
})
.then (url => {
// Fetch this second url
let data = api.get(url)
sessions.session_id = 'SUCCESS'
// Return the whole data object
return data
})
.catch(err => {
console.log("ERR", err)
sessions.session_id = 'ERROR'
})
return result
}
asyncFunc()
.then(res => {
console.log("URL", url)
})
This code tracks the state of the functions and stores them to the global sessions object - but the session_id isn't being passed back for inspection of status while the function is 'in-flight'.
One option I'm considering is adding the session_id as a property of the promise when it is returned, so this can be inspected - however I'm not sure if adding a property to a promise is a risky/hacky thing to do? Something like (simplified from above):
const asyncFunc = () => {
// Get a new id for state tracking
let session_id = getID()
sessions.session_id = 'PENDING'
// Fetch the first url
let result = api.get(url)
.then(...)
.then(...)
.catch(...)
// Add the session_id to the promise
result.session_id = session_id
return result
}
let func = asyncFunc()
let status =sessions[func.session_id]
func.then(...)
Any thoughts on the validity of this approach? I can see that I would probably also need to push the session id into the final return value as well, (so that the property exists in both the promise, and the resulting value of the resolved/rejected promise).
Alternatively, any other ways of handling this?
The obvious one is to make the function always return an array of arguments (promise and session_id) but I'd prefer to avoid having to always do e.g.:
let func = asyncFunc()
let status =sessions[func[1]]
func[0].then(...)

Related

Question about asynchronous JavaScript with Promise

Here I have a function that takes an array of string that contains the user names of github accounts. And this function is going to return an array of user data after resolving. There should be one fetch request per user. and requests shouldn’t wait for each other. So that the data arrives as soon as possible. If there’s no such user, the function should return null in the resulting array.
An example for the input would be ["iliakan", "remy", "no.such.users"], and the expected returned promise after resolving would give us [null, Object, Object], Object being the data that contained info about a user.
Here is my attempt to solve this question.
function getUsers(names) {
return new Promise(resolve => {
const array = [];
const url = "https://api.github.com/users/";
const requests = names.map(name => {
const endpoint = `${url}${name}`;
return fetch(endpoint);
});
Promise.all(requests).then(reponses => {
reponses.forEach(response => {
if (response.status === 200) {
response.json().then(data => {
array.push(data);
});
} else {
array.push(null);
}
});
resolve(array);
});
});
}
It does work, i.e. returning an array [null, Object, Object]. And I thought it fulfilled the requirements I stated above. However, after looking at it closely, I felt like I couldn't fully make sense of it.
My question is, look at where we resolve this array, it resolved immediately after the forEach loop. One thing I don't understand is, why does it contain all three items when some of the items are pushed into it asynchronously after the json() is finished. what I mean is, in the case where response.status === 200, the array is pushed with the data resolved from json(), and I would assume this json() operation should take some time. Since we didn't resolve the array after json() operation is finished, how come we still ended up with all data resolved from json()?
Promise.all(requests).then(reponses => {
reponses.forEach(response => {
if (response.status === 200) {
response.json().then(data => {
array.push(data); <--- this should take some time
});
} else {
array.push(null);
}
});
resolve(array); <--- resolve the array immediately after the `forEach` loop
});
});
It looks to me like the array we get should only have one null in it since at the time it is revolved, the .json() should not be finished
You're right, the result is pushed later into the array.
Try to execute this:
const test = await getUsers(['Guerric-P']);
console.log(test.length);
You'll notice it displays 0. Before the result is pushed into the array, its length is 0. You probably think it works because you click on the array in the console, after the result has arrived.
You should do something like this:
function getUsers(names) {
const array = [];
const url = "https://api.github.com/users/";
const requests = names.map(name => {
const endpoint = `${url}${name}`;
return fetch(endpoint);
});
return Promise.all(requests).then(responses => Promise.all(responses.map(x => x.status === 200 ? x.json() : null)));
};
You should avoid using the Promise constructor directly. Here, we don't need to use it at all.
const url = "https://api.github.com/users/";
const getUsers = names =>
Promise.all(names.map(name =>
fetch(url + name).then(response =>
response.status === 200 ? response.json() : null)));
getUsers(["iliakan", "remy", "no.such.users"]).then(console.log);
The Promise constructor should only be used when you're creating new kinds of asynchronous tasks. In this case, you don't need to use the Promise constructor because fetch already returns a promise.
You also don't need to maintain an array and push to it because Promise.all resolves to an array. Finally, you don't need to map over the result of Promise.all. You can transform the promises returned by fetch.
The thing is that because json() operation is really quick, especially if response data is small in size it just has the time to execute. Second of all as objects in JavaScript passed by reference and not by value and Array is a object in JavaScript, independently of execution time it'll still push that data to the array even after it was resolved.

Promise returning undefined and status pending

I'm adding some functions to an Angular App and here is the thing: I'm trying to use a function that creates a promise to get data from the server, but every time I try to use it, it returns undefined. I've "debugged" with console.log printing my variable with the result of my function as value and it prints Promise{'pending'}
Here is the function of the promise and the variable I'm trying to assign.
all_allies(text = ''){
return new Promise((resolve, reject) => {
const _text = text ? /${text} : ''
const path = `${this.env.apiPath}/all_allies${_text}`
this.$http
.get(path)
.then(response => {
const { data } = response
resolve(data)
return data;
})
.catch(error => reject(error))
})
Variable
let allies = this.AliadosFactory.all_allies();
As you can see the function and the variable are in different scripts.
I've tried using await reserved word but still doesn't work
Can you try this method?
let allies = await this.AliadosFactory.all_allies();
console.log(allies);
or like this?
this.AliadosFactory.all_allies().then(allies => console.log(allies);
I sure it should work,
Hope this helps.
Have nice day :)
That's because when you perform assignment the Promise is not resolved / rejected yet.
There are two simple solutions:
1. Using then()
this.AliadosFactory.all_allies().then(result => console.log(result));
2. Using async/await
(note that you need an async method in your class)
async foo() {
let allies = await this.AliadosFactory.all_allies();
console.log(allies);
}
Also in all_allies() you don't need to return the value after calling the resolve() method;

Using composition in JavaScript

I want to make a request and cache it, in a functional style.
const req = (uri) =>
(console.log(`requesting: ${uri}`), Promise.resolve({ status: 200 }));
const cache = (fn) => (...args) =>
fn(...args).then((result) => { console.log('caching:', result) });
const cachedReq = cache(req);
cachedReq('example.com/foo');
Two questions:
Is this code idiomatic?
How can I supply logic to generate the cache key from the result, while maintaining separation of concerns? For example, I might use req to retrieve different kinds of resource which need different logic to generate the key to be used in the cache. How should I supply this key-generation logic to the cache function?
Edit:
In reality, the URI should be the key (thanks to #epascarello). I chose a poor example. But I'd like to ask about the more general case, where logic needs to be supplied "down composition", while maintaining decent separation of concerns.
You almost close to achieve your goal, you are in the right direction, with composition concept. maybe this code can help you to make your goal come true.
Let's simulate your req function like so:
var req = (uri) => {
console.log("inside req", uri);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ status: 200 });
}, 3000);
});
}
then you have the cacheFunc version as:
var withCache = (promiseFunc) => {
const cache = {};
return (...args) => {
// suppose first param is uri
var uri = args[0];
return new Promise((resolve, reject) => {
if (cache.hasOwnProperty(uri)) {
return resolve(cache[uri]);
}
promiseFunc(...args).then((data) => {
cache[uri] = data;
resolve(data);
}).catch(reject);
});
}
}
as you can see, you need to create and cache object into the first function, so this is a a little similar to Currying in JS, so you need to wrap your req (that is a promise) wrapped into another promise from the cache version, so before execute the req function, you need to verify if some response exists into cache with the same uri key, if it is, so resolve inmmediatly the promise, else execute the req function, once you receive the response cache the response and resolve the cache promise version.
So you can use it like so:
var cacheReq = withCache(req);
cacheReq('https://anywhere.com').then(console.log.bind(null, 'response')).catch(console.log.bind(null, 'error response'));
you will notice that in the first time you promise wait until 3 seconds to resolve the req, in the second call the promise will resolve the promise ASAP because of cache, if you try with another uri it will wait 3 seconds again and will cache the response to use it the next time.
Hope it can help you.
You can use a combination of a Map and the Request constructor:
// I'll be using ramda for object equality, but any
// deepEquals checker should work.
const R = window.R;
const genRequest = ((cache, eqComparator) => {
return (url, fetchOpts={}) => {
const key = {url, fetchOpts};
const alreadyHave = [...cache.keys].find(x => eqComparator(x, key));
if (alreadyHave) return cache.get(alreadyHave);
const req = new Request(url, fetchOpts);
cache.set(key, req);
return req;
};
})(new Map(), R.equals);
const req = genRequest('http://www.google.com');
fetch(req)
.then(...)
.catch(...);
Some nice properties fall out of this:
Each request is constructed only once but can be repeatedly fetched.
No side-effects until you fetch: creating the request and fetching it are separate.
...thus, concerns are about as separated as they can be.
You could re-jigger parameter application to easily support custom equality comparisons using the same cache.
You can use the same strategy to cache the results of a fetch, separately from caching the requests.

Global Variable not carrying over- Javascript FCF

I'm using firebase cloud functions in which I define a variable commentIdSpecific. When I log it inside the function: -- console.log(comm id ${commentIdSpecific}); -- it prints its value. When I try to print it here: -- console.log(test of variables inisde of post: ${usernameWhoOwnsThePost}, uwotpi: ${commentIdSpecific}) -- it returns undefined. I've looked at three websites talking about global vars and it doesn't seem any different from what I have here.
How do I go about getting the value in the second print statement to be in the first? Thanks in advance.
var commentIdSpecific;
db.ref(`/users/${usernameWhoOwnsThePost}/posts/${usernameWhoOwnsThePostID}/comments`).once('value').then(snap => {
commentIdSpecific = snap.val();
let ids = [];
for (var id in snap.val()) {
ids.push(id);
}
let lastValueId = ids[ids.length - 1]
console.log(`last id value ${lastValueId}. UserPost: ${usernameWhoOwnsThePost}. user owner post id: ${usernameWhoOwnsThePostID}...`);
commentIdSpecific = lastValueId;
console.log(`comm id ${commentIdSpecific}`);
return commentIdSpecific;
}).catch(err => {
console.log(err);
});
var commentPoster;
db.ref(`/users/${usernameWhoOwnsThePost}/posts/${usernameWhoOwnsThePostID}/comments/${commentIdSpecific}/comment`).once('value').then(snap => {
commentPoster = snap.val();
console.log(`commentPoster: ${snap.val()}`);
console.log(`test of variables inisde of post: ${usernameWhoOwnsThePost}, uwotpi: ${commentIdSpecific}`)
return commentPoster
}).catch(err => {
console.log(err);
});
once() is asynchronous and returns immediately with a promise that indicates when the async work is complete. Likewise, then() returns immediately with a promise. The callback you pass to then() is executed some unknown amount of time later, whenever the results of the query are finished. Until that happens, your code keeps executing at the next line, which means commentIdSpecific will be undefined when it's first accessed.
You need to use a promise chain to make sure the work that depends on the results of async work is only accessed after it becomes available.
You may want to watch the videos on JavaScript promises on this page in order to better learn how they're used in Cloud Functions. It's absolutely critical to understand how they work to write effective code.
https://firebase.google.com/docs/functions/video-series/
You should make the second db.ref call in promise chain once the first promise resolved like this:
db.ref(`/users/${usernameWhoOwnsThePost}/posts/${usernameWhoOwnsThePostID}/comments`).once('value')
.then(snap => {
commentIdSpecific = snap.val();
let ids = [];
for (var id in snap.val()) {
ids.push(id);
}
let lastValueId = ids[ids.length - 1]
console.log(`last id value ${lastValueId}. UserPost: ${usernameWhoOwnsThePost}. user owner post id: ${usernameWhoOwnsThePostID}...`);
commentIdSpecific = lastValueId;
console.log(`comm id ${commentIdSpecific}`);
return commentIdSpecific;
})
.then(commentIdSpecific => {
db.ref(`/users/${usernameWhoOwnsThePost}/posts/${usernameWhoOwnsThePostID}/comments/${commentIdSpecific}/comment`).once('value').then(snap => {
commentPoster = snap.val();
console.log(`commentPoster: ${snap.val()}`);
console.log(`test of variables inisde of post: ${usernameWhoOwnsThePost}, uwotpi: ${commentIdSpecific}`)
return commentPoster
}).catch(err => {
console.log(err);
});
})
.catch(err => {
console.log(err);
});
once() is an async operation so it might possible that console.log(test of variables inside of post: ${usernameWhoOwnsThePost}, uwotpi: ${commentIdSpecific}) executed before commentIdSpecific = snap.val(); and commentIdSpecific = lastValueId;
So what you need to do is first let db.ref(/users/${usernameWhoOwnsThePost}/posts/${usernameWhoOwnsThePostID}/comments) be completed and then make call to db.ref(/users/${usernameWhoOwnsThePost}/posts/${usernameWhoOwnsThePostID}/comments/${commentIdSpecific}/comment) in next .then() in the chain.
As you are returning commentIdSpecific form first .then() So it will be available as param in second .then().
https://javascript.info/promise-chaining will help you to dig .then chaining more into deep.

wait on a promise for each iteration in nodejs

I am developing a nodejs application that needs to get settings from an array(in a settings object), call a rest api based on the settings and write the response to mongodb and repeat this for the next setting in the array.
Here is a simplified version of the application
var setting //global
process(){ //top level function
for(let s of config.settings){
setting = s;
getData();
}
}
function getData(){
init()
.then(makeRequest) // constructs and makes the rest api call
.then(insert) // writes the response to the db
.catch(function(err){
// logs err
}
}
Running it, only the data for the last setting (in the array) is written to the db and this happens for each iteration. Basically the same data is written on the db for as many iterations.
The problem I can see from this is that the for loop finishes executing, before the promises return with the value.
I have seen some examples of async.for
Any suggestions on fixing this. How do you go about designing this kind of a flow?
You can bind the settings to each function call to preserve the value. looks like you'd have to refactor though as the value would be passed in as an argument though i'm not sure if your code is pseudo code or actual code.
async await would work as well but would take longer as it would pause execution at each api call.
You should return an object or array that you can use to store an internal state for your request. Please see the example for how it works.
Also never set a global variable to store your state, with your function being asynchronous the value may not be what you expect it to be.
With this approach you are passing { init } for the first promise, then { init, request } for the next so you have the response from each part of your promise chain that you can use to make further requests.
// return an object to store the state on init
const init = () =>
new Promise((res, rej) => res({
init: 'initted'
}))
// pass init and the request to the next function in the chain
const makeRequest = ({ init }) =>
new Promise((res, rej) => res({
init,
request: {
msg: 'this is the response',
id: 33
}
}))
// insert stuff from the request
// then return the data to the next query
const insert = ({ init, request }) =>
new Promise((res, rej) => res({
request,
init,
created_at: Date.now()
}))
const trace = name => x => (console.log(name, x), x)
function getData(){
return init() // return your promise so you can chain it further
.then(trace('after init'))
.then(makeRequest)
.then(trace('after request'))
.then(insert)
.then(trace('after insert'))
.catch(console.error)
}
// call you function
getData()
// since the promise is returned we can continue the chain
.then(state => console.log({ state }))
<script src="https://codepen.io/synthet1c/pen/KyQQmL.js"></script>
All of your loop will have executed by the time the callbacks are coming in. So settings will be the last value.
Instead of relying on globals, pass setting into getData, for example.

Categories

Resources