Multiple Promises: Get back the input that was used to create them - javascript

I have an async function getContextUrl(id: string): Promise<string> that will return an url: string for a given id: string. Because the function is async, the url is wrapped in a Promise.
I now want to map a resolved url to the original id that was used. So far I have the following:
const urlMap: {[key: string]: string} = {}; // map to store url->id as key->value
const ids: string = ['foo', 'bar']; // some IDs
const contextUrlPromises: Array<Promise<string>> = ids.map((id: string) => getContextUrl(id));
Promise.all(contextUrlPromises).then((urls: string[]) => {
// what do I do here? How can I map back to the `id` that belongs to this `url`?
})
// my getUrl function
async function getContextUrl(id: string): Promise<string> {
// driver.getUrl is actually the async function from WebdriverIO, but that shouldn't be relevant for my problem
return driver.getUrl();
}
My problem is: once the urls are resolved, because of their asynchronous nature, I don't know which url belongs to which id. As you can see, my approach is to populate a key/value object urlMap with the values. But maybe there's a better way...
Bonus: I actually only need the first resolved url which contains a certain string, so there is room for further optimization because the calls to getContextUrl() can stop, as soon as such an url has been resolved. Something like this:
const contextPromises: Array<Promise<{ id: string, url: string}>> = ids.map((id: string) => getContextUrl(id).then((context: { id: string, url: string}) => {
if (context.url.includes("foo")){
return context.id;
}
});
Promise.race(contextPromises).then((firstId: string) => {
// do something with the first ID whose URL contains "foo"
})

The data returned by Promise.all are in the same order as the data used to called it.
In the following example, we are going to use the index of the url to find the related id.
Playground
// map to store url->id as key->value
const urlMap: {
[key: string]: string;
} = {};
// some IDs
const ids: string[] = ['foo', 'bar'];
// my getUrl function
function getContextUrl(id: string): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => resolve('foo'), 0);
});
}
const contextUrlPromises: Promise<string>[] = ids.map((id: string) => getContextUrl(id));
(async() => {
const urls: string[] = await Promise.all(contextUrlPromises);
urls.forEach((x, xi) => {
console.log(`The urls ${x} belongs to the id ${ids[xi]}`);
});
})();
In JavaScript :
// map to store url->id as key->value
const urlMap = {};
// some IDs
const ids = ['foo', 'bar'];
// my getUrl function
function getContextUrl(id) {
return new Promise((resolve) => {
setTimeout(() => resolve('foo'), 0);
});
}
const contextUrlPromises = ids.map(id => getContextUrl(id));
(async() => {
const urls = await Promise.all(contextUrlPromises);
urls.forEach((x, xi) => {
console.log(`The urls ${x} belongs to the id ${ids[xi]}`);
});
})();

In the return statement of getContextUrl() you can return an object containing both the id and the url. In the example below I have formatted it similar to urlMap. This way you can add it to the object without any modification.
let urlMap: {[key: string]: string} = {}; // map to store url->id as key->value
const ids: string = ['foo', 'bar']; // some IDs
const contextUrlPromises: Array<Promise<string>> = ids.map((id: string) => getContextUrl(id));
Promise.all(contextUrlPromises).then((urls: string[]) => {
urlMap = {...urlMap, ...urls};
})
// my getUrl function
async function getContextUrl(id: string): Promise<string> {
// driver.getUrl is actually the async function from WebdriverIO, but that shouldn't be relevant for my problem
return {[id]: driver.getUrl()};
}

Related

Selecting specific field in array of objects returns undefined but logging whole returning whole array gives ok values

I'm trying to get podcast data with `SpotifyAPI. I am fetching the data on my node.js server and sending it as a json to client. I receive a typed object and trying to push this object to an array as I want to have an array of 50 podcasts each of them being an object:
export interface Episode {
name: string;
description: string;
date: string;
img: string;
url: string
};
I'm sending token (to whole function component) and ids of shows I want to fetch
const fetchEpisodesData = async (ids: string[]) => {
let arr: any[] = [];
ids.forEach(async (id, index) => {
const response = await fetch(
`http://localhost:8080/podcastEpisodes?access_token=${tokenCode}&id=${id}`
)
const dataResponse = await response.json();
arr.push(dataResponse)
// )
});
console.log(arr);
};
When I console.log(arr) it shows what I pretty much want and an array of objects but when I console.log(arr[1]) it returns undefined.
Any ideas?
Here is my code of the fetchinng function
export const useFetch = (tokenCode: string) => {
// console.log(tokenCode);
let array: any[] = [];
const [data, setData]: any = useState([]);
const [loading, setLoading] = useState(true);
const getData = async (id: string) => {
const response = await fetch(
`http://localhost:8080/podcastEpisodes?access_token=${tokenCode}&id=${id}`
);
const dataResponse = await response.json();
return dataResponse;
};
const fetchdata = async () => {
const response = await fetch(
`http://localhost:8080/podcast?access_token=${tokenCode}`
);
const dataResponse = await response.json();
// const item = dataResponse.results[0];
const myData = Object.keys(dataResponse).map((key) => {
return dataResponse[key];
});
// console.log(myData[0].show.id)
const idData = myData.map((data) => {
return data.show.id as string;
});
// console.log(idData, 'idData')
return idData;
// console.log(dataResponse);
};
const fetchEpisodesData = async (ids: string[]) => {
let arr: any[] = [];
ids.forEach(async (id, index) => {
const response = await fetch(
`http://localhost:8080/podcastEpisodes?access_token=${tokenCode}&id=${id}`
)
const dataResponse = await response.json();
arr.push(dataResponse)
// )
});
console.log(arr);
};
useEffect(() => {
fetchdata().then((res) => {
// console.log(res);
fetchEpisodesData(res);
});
}, [tokenCode]);
return { data, loading };
};
The issue here is a bit subtle.
You are pushing only one element, the dataResponse object, to your array arr here:
arr.push(dataResponse)
Since you don't push any other value, and arrays in Javascript are 0-indexed, you will only find your value in arr[0], but arr[1] will return undefined.
Now, if you wanted to fill your array arr with an array that would be getting in dataResponse, then you should spread the array when doing .push(), so you'll be pushing all elements found in such dataResponse array object.
Like so:
const dataResponse = await response.json();
// this works only **if** dataResponse is an array
// and you want to add all elements to your 'arr' array
arr.push(...dataResponse);

React typescript Promise with webservice and depending another function in loop

On button click I am looping through record id's pass that 1 by 1 to webservice which will return XML data where in I will get ID of another record which I will pass to another webservice which will return result success. After finish this I want to show message success
Function defination
const GetRunningWorkflowTasksForCurrentUserForListItemRequest = (
absoluteUrl: string,
itemId: number,
listName: string,
callback
) => {
const soapURL = `${absoluteUrl}/example.asmx?op=GetListItem`
const soapRequest = `SOAP Request`
getWFData(soapURL, soapRequest, callback) // Get XML with call back function parameter where we will process data
}
Second Function Call
const getWFData = (soapURL: string, soapRequest: string, callback) => {
const xmlhttp = new XMLHttpRequest()
xmlhttp.open("POST", soapURL, true)
xmlhttp.onreadystatechange = () => {
if (xmlhttp.readyState === 4) {
if (xmlhttp.status === 200) {
callback(xmlhttp.responseText)
}
}
}
xmlhttp.setRequestHeader("Content-Type", "text/xml")
xmlhttp.send(soapRequest)
}
First Function Call with loop
const approveSelected = (ids: number[]) => {
ids.forEach((val, idx) => {
const absoluteUrl = props.context.pageContext.web.absoluteUrl
// First function
GetRunningWorkflowTasksForCurrentUserForListItemRequest(
absoluteUrl,
val,
"Temp",
GetRunningWorkflowTasksForCurrentUserForListItemResponse //XML Response
)
})
}
Third Function where we got XML response
const GetRunningWorkflowTasksForCurrentUserForListItemResponse = (response: any) => {
const parser = require("fast-xml-parser")
const absoluteUrl = props.context.pageContext.web.absoluteUrl
if (parser.validate(response) === true) {
const jSONObj = parser.parse(response)
const spTaskId =
jSONObj["soap:Envelope"]["soap:Body"].GetRunningWorkflowTasksForCurrentUserForListItemResponse
.GetRunningWorkflowTasksForCurrentUserForListItemResult.UserTask.SharePointTaskId
processFlexiTaskRequest2(
absoluteUrl,
"Approve",
spTaskId,
"Workflow Tasks",
processFlexiTaskResponse2Response, //XML Response function
""
)
}
}
Forth and Final call for inside loop
const processFlexiTaskResponse2Response = (response: any) => {
const parser = require("fast-xml-parser")
if (parser.validate(response) === true) {
const jSONObj = parser.parse(response)
const result =
jSONObj["soap:Envelope"]["soap:Body"].ProcessFlexiTaskResponse2Response.ProcessFlexiTaskResponse2Result
}
}
I am really confuse, How can I make chain with promise and show confirm once loop finish. Please help
Two key steps are required to make this work with promises:
Convert each id to a promise that either resolves to the respective response or alternatively to the result of processFlexiTaskResponse2Response
Use Promise.all() to combine all these promises to one promise that resolves when all of the per-id promises are resolved.
This is the most relevant part
const approveSelected = (ids: number[]) => {
const promises = ids.map((val, idx) => {
const absoluteUrl = props.context.pageContext.web.absoluteUrl
// First function
return GetRunningWorkflowTasksForCurrentUserForListItemRequest(
absoluteUrl,
val,
"Temp"
).then(response => {
// transform response via
// GetRunningWorkflowTasksForCurrentUserForListItemResponse
// and
// processFlexiTaskResponse2Response
});
});
Promise.all(promises).then(results => {
// the code here executes once all results are there.
});
}
You have to change GetRunningWorkflowTasksForCurrentUserForListItemResponse and
processFlexiTaskResponse2Response to simply return their respective result so you can chain them.
To make GetRunningWorkflowTasksForCurrentUserForListItemRequest return a promise you can change it like this:
const GetRunningWorkflowTasksForCurrentUserForListItemRequest = (
absoluteUrl: string,
itemId: number,
listName: string,
callback
) => new Promise((resolve, reject) => {
const soapURL = `${absoluteUrl}/example.asmx?op=GetListItem`
const soapRequest = `SOAP Request`
getWFData(soapURL, soapRequest, resolve);
});

Angular 7 adding data to result of http request

I get an array of IDs (assetIDs) and using those IDs I want to ask for data.
For each http request I'm receiving one or more datasets.
I want to add the request ID to each dataset and then return the data.
Getting and returning the data works just fine, but I don't know how to add that assetID to the dataset.
When I do it like in the following code snippet, I only get the first dataset of each ID. (Of course...because of the [0]). But how can I iterate over all datasets?
getData(assetIds: Array<string>): Observable<any> {
const data = assetIds.map(assetId => {
// for each assetId
const path = this.serverUrl + '?' + 'assetid=' + assetId;
return this.httpClient.get(path).pipe(
map((res: any[]) => {
return {
name: res[0].name,
type: res[0].type,
asset: assetId
};
}));
});
// return combined result of each assetId request
return forkJoin(data);
}
I also tried the following, but I don't get any data when doing this:
getData(assetIds: Array<string>): Observable<any> {
const data = assetIds.map(assetId => {
// for each assetId
const path = this.serverUrl + '?' + 'assetid=' + assetId;
return this.httpClient.get(path).pipe(
map((res: any[]) => {
const resultArray = [];
res.forEach(element => {
const row = {
name: res[element].name,
type: res[element].type,
asset: assetId
};
resultArray.push(row);
});
return resultArray;
}));
});
// return combined result of each assetId request
return forkJoin(data);
}
Your second aproach seems fine. I believe the problem is that you are using the rxjs operator forkJoin.
As RXJS docs say, this operator emits value when
When all observables complete, emit the last emitted value from each.
You basically have 2 options, change operator forkJoin to zip
After all observables emit, emit values as an array
Or add the take(1) operator after the map on pipe. Take operator will complete the observable after 1 value is emmited, permitting forkJoin to emit its values
You can use map also on your result array. Something like this:
const data = assetIds.map(assetId => {
// for each assetId
const path = this.serverUrl + '?' + 'assetid=' + assetId;
return this.httpClient.get(path).pipe(
map((res: any[]) => res.map(item => {
return {
name: item.name,
type: item.type,
asset: assetId
}
})));
});
// return combined result of each assetId request
return forkJoin(data);
}
Thanks for your replies. I tried everything but I guess the problem was that I used "element" as index within the array. The following code now works just fine:
return this.httpClient.get(path)
.pipe(
map((datasets: AssetFilesTableItem[]) => {
const result: AssetFilesTableItem[] = [];
let i = 0;
datasets.forEach(element => {
const dataset: AssetFilesTableItem = {
name: datasets[i].name,
type: datasets[i].type,
size: datasets[i].size,
timestamp: datasets[i].timestamp,
created: datasets[i].created,
updated: datasets[i].updated,
asset: assetId
};
result.push(dataset);
i++;
});
return result;
}));

Is Await inside a Promise ignored?

I have this simple function below, where you would pass in a String as parameter which would return a Promise that would only resolve to either empty object or filled object, based on a firestore sub-collection query.
The problem is the very last resolve({}) is called even if I've invoked an await on the previous statement, what could possibly be happening?
getSubCollections = (id: string): Promise<any> => {
const subCollectionPromise = new Promise(async (resolve: Function) => {
if (typeof this.subCollectionKeys === 'undefined') {
resolve({});
}
const collections = {};
await this.subCollectionKeys.forEach(async (key: string) => {
const subCollection = await this.getCollection()
.doc(id)
.collection(key)
.get();
const firebaseObj = await subCollection.docs.map((obj: Object) =>
this.getShallowData(obj),
);
const fromFirebase = fromFirebaseObj(firebaseObj);
if (!fromFirebase.empty) {
collections[key] = fromFirebase;
}
resolve(collections);
});
resolve({}); /// WHY IS THIS CALLED EVEN IF THERE IS AWAIT ON TOP???
});
return subCollectionPromise;
};

How to add "fallback" value to optional function argument in flow?

I have following function utalising flow types
push = (pathname: string, data?: Object) => {
const history = [...this.state.history, pathname];
this.setState({
history,
pathname,
data
});
};
In normal javascript I would be able to do something like (pathname, data = null) where null would be used for data if it wasn't provided, but I can't figure out syntax for this when using flow types.
let push = (pathname: string, data?: Object = {a: 1}) => {
// ...
};
In addition, you can also use default values with destructuring together:
type Arg = { prop: number };
const func = ({ prop = 1 }: Arg) => prop;

Categories