async Iterate over API results - javascript

I am calling an API which returns results in pages and I am trying to find an 'elegant' way of retrieving them.
Ideally I want to consume them like this:
let results = api.get();
for await (const page of results) {
// do stuff with page
}
I can active close to this using a generator function like this:
class Results {
constructor(url, token) {
this.url = url;
this.token = token;
}
async *page() {
let url = this.url;
while (true) {
const response = await fetch(url, {
headers: { 'Authorization': 'Bearer ' + this.token }
});
const data = await response.json();
yield data.values;
if (!data.next) return;
url = data.next;
}
}
}
And calling it like:
for await (const page of results.page()) {
// do stuff with page
}
I have tried to do it with a [Symbol.iterator] like this, but cannot get it to work:
[Symbol.iterator]() {
let that = this;
return {
next: async function() {
if (!that.page) {
that.page = that.url;
return {done: true};
}
const response = await fetch(that.page, {
headers: { 'Authorization': 'Bearer ' + that.token }
});
const data = await response.json();
that.page = data.data.next;
return {
value: data,
done: false
}
}
}
}
This issue is I need to get the link to the next page from the current page to determine if there is a next page, but as its a promise i cannot access it in the function.
Any ideas how to get the iterator working?

Following advice here is a working function. [Symbol.asyncIterator] made all the difference. Thanks:
[Symbol.asyncIterator]() {
let that = this;
return {
page: that.url,
token: that.token,
next: async function() {
if (!this.page) {
this.page = that.url;
return {done: true};
}
const response = await fetch(this.page, {
headers: { 'Authorization': 'Bearer ' + this.token }
});
const data = await response.json();
this.page = data.next;
return {
value: data,
done: false
}
}
}
Now that its working ideally I just want to be able to iterate through all results and not know about pages so here is a working solution to this for info:
[Symbol.asyncIterator]() {
let that = this;
return {
page: that.url,
token: that.token,
values: [],
next: async function() {
if (!this.page && this.values.length === 0) {
this.page = that.url;
return {done: true};
}
if (this.values.length > 0) {
return {
value: this.values.pop(),
done: false
}
}
const response = await fetch(this.page, {
headers: { 'Authorization': 'Bearer ' + this.token }
});
const data = await response.json();
this.page = data.next;
this.values = data.values;
if (this.values.length === 0) {
return { done: true }
}
return {
value: this.values.pop(),
done: false
}
}
}
This code can be simplified by using an async generator function like so:
async *[Symbol.asyncIterator]() {
let url = this.url;
const getPage = url =>
fetch(url, this.header)
.then(response => response.json())
.then(data => ({
next: data.next,
values: data.values
}));
while(url) {
const page = await getPage(url);
for (const value of page.values) {
yield value;
}
url = page.next;
}
}
So the full class looks like this:
class Response {
constructor(url, token) {
this.url = url;
this.header = {
headers: {
Authorization: `Bearer ${token}`
}
};
}
async* [Symbol.asyncIterator]() {
let url = this.url;
const getPage = url =>
fetch(url, this.header)
.then(response => response.json())
.then(data => ({
next: data.next,
values: data.values
}));
while (url) {
const page = await getPage(url);
for (const value of page.values) {
yield value;
}
url = page.next;
}
}
}
Which allows you to easily loop through paged API results like this:
for await (const item of response) {
...
}

Related

How to access variable inside a for-in loop within an async function (javascript/react)?

So I made this little fetch function in a React project to loop through the returned object and give me a number of something. I'm trying to get back the value of profileCount to access it outside of the async function. How would I do that?
useEffect(() => {
async function getProfiles() {
try {
const url = '';
let h = new Headers();
h.append('Accept', 'application/json');
let encoded = window.btoa('superuser:superuser');
let auth = 'Basic ' + encoded;
h.append('Authorization', auth);
const response = await fetch(url, {
method: 'GET',
mode: 'cors',
headers: h,
credentials: 'same-origin',
});
const data = await response.json();
let results = data.results;
let profileCount = 0;
for (let key in results) {
let innerObject = results[key];
for (let newKey in innerObject) {
if (innerObject[newKey].indexOf('pages/ProfilePage') > -1) {
profileCount += 1;
return profileCount;
}
}
}
} catch (err) {
console.log('test' + err);
}
}
getProfiles();
}, []);
Your code is good, just relocate the return statement after the for.
for (let key in results) {
let innerObject = results[key];
for (let newKey in innerObject) {
if (innerObject[newKey].indexOf('pages/ProfilePage') > -1) {
profileCount += 1;
}
}
}
return profileCount;
The workflow is the following,
useEffect(async () => {
async function getProfiles() {}
const count = await getProfiles();
}, []);
But you might have to do this to avoid warning,
useEffect(() => {
(async () => {
let response = await fetch('api/data')
})();
}, [])

componentDidMount() returns an undefined value

Goal
My goal is to call componentDidMount() function to return some values from another method called getUserPlaylists().
Problem
The problem I am encountering is that the componentDidMount() shows me value of undefined and getUserPlaylists() shows me a result of an array.
Actual result
Code
Within Spotify.js file I have the following code:
const clientId = 'Cleint ID Here';
const redirectUri = 'http://localhost:3000/';
let accessToken;
let userId;
const Spotify = {
getAccessToken() {
if (accessToken) {
return accessToken;
}
const accessTokenMatch = window.location.href.match(/access_token=([^&]*)/);
const expiryInMatch = window.location.href.match(/expires_in=([^&]*)/);
if (accessTokenMatch && expiryInMatch) {
accessToken = accessTokenMatch[1];
const expiresIn = Number(expiryInMatch[1]);
window.setTimeout(() => accessToken = '', expiresIn * 10000);
window.history.pushState('Access Token', null, '/');
return accessToken;
} else {
const accessUrl = `https://accounts.spotify.com/authorize?client_id=${clientId}&response_type=token&scope=playlist-modify-public&redirect_uri=${redirectUri}`;
window.location = accessUrl;
}
},
async getUserPlaylists() {
await Spotify.getCurrentUserId().then(userId => {
const accessToken = Spotify.getAccessToken();
const headers = { Authorization: `Bearer ${accessToken}` };
fetch(` https://api.spotify.com/v1/users/${userId}/playlists`, {
headers : headers
})
.then(res => res.json())
.then(res => {
if(!res.items) {
return [];
} else {
console.log(res.items)
return res.items;
}
})
})
},
getCurrentUserId() {
if (userId) {
return new Promise((resolve) => {
resolve(userId);
})
} else {
return new Promise((resolve) => {
const accessToken = Spotify.getAccessToken();
const headers = { Authorization: `Bearer ${accessToken}` };
return fetch("https://api.spotify.com/v1/me", { headers: headers })
.then(res => res.json())
.then(jsonRes => {
userId = jsonRes.id;
resolve(userId);
});
})
}
}
}
export { Spotify };
Summary
I have 3 objects that can be called as methods within my app.js file.
Here is how I call the componentDidMount() within my app.js file:
async componentDidMount() {
const val = await Spotify.getUserPlaylists();
console.log(val)
}
Expected result
The componentDidMount() should return the same value as getUserPlaylists()
Question
I don't understand why componentDidMount() is returning value of undefined?
Cause you're not returning anything from getUserPlaylists
async getUserPlaylists() {
// here return missed
return await Spotify.getCurrentUserId().then(userId => {
const accessToken = Spotify.getAccessToken();
const headers = { Authorization: `Bearer ${accessToken}` };
// here return too
return fetch(` https://api.spotify.com/v1/users/${userId}/playlists`, {
headers : headers
})
.then(res => res.json())
.then(res => {
if(!res.items) {
return [];
} else {
console.log(res.items)
return res.items.map(playlist => ({
playlistId: playlist.id,
playListName: playlist.name
}));
}
})
})
},
You can simply use the below code, which does the same
async getUserPlaylists() {
// here return missed
try {
const userId = await Spotify.getCurrentUserId()
const accessToken = Spotify.getAccessToken();
const headers = { Authorization: `Bearer ${accessToken}` };
// here return too
const result = await fetch(` https://api.spotify.com/v1/users/${userId}/playlists`, { headers })
const res = await result.json()
if(!res.items) return [];
console.log(res.items)
return res.items.map(playlist => ({ playlistId: playlist.id, playListName: playlist.name }));
} catch(err) {
console.log({ err })
}
}

problem awaiting the callback given to a function

I have a function who send a message to the server to get the answer and if the answer is true I want my application to send an error to the user. The problem is that I can't manage to await the callback in the Fetch function I wrote.
This is the function who send the question to the server.
async donglePaired(){
if (Platform.OS !=='ios'){
var pairedDevices = await BluetoothScanner.getPairedDevices();
console.log('Sending........');
let data={
data:pairedDevices,
};
new Api().fetch("bluetoothCheck",{devices:JSON.stringify(data),userid:this.state.probe.UID},(result) => {
if (!result.err) return false;
console.log("Dongle already paired");
return true;
//logNetworkState
});
}
}
This is the Api.fetch function i wrote
fetch(action,data,cb){
let url=Config.URL+"?version="+Config.VERSION+"&action="+action;
let post="";
let formData=new FormData();
for(let k in data) formData.append(k,data[k]);
for(let k in data) post+="&"+k+"="+encodeURIComponent(data[k]).replace(/%20/g,'+');
console.log(url+post);
console.log(url);
if (data.batch) console.log(data.batch);
let sending=true;
fetch(url,{
method: 'post',
body: formData
})
.then(function(response){
if (true) return response.json();
let txt=response.text();
console.log(txt);
return JSON.parse(txt);
})
.then(
(result)=>{
if (!sending) return;
sending=false;
console.log(JSON.stringify(result));
if (cb) cb(result);
},
(error)=>{
if (!sending) return;
sending=false;
console.log("fetch error");
console.log(error);
if (cb) cb();
}
);
setTimeout(()=>{
console.log("http timeout")
if (!sending) return console.log("nothing to abort");
if (cb) cb();
},Config.HTTP_TIMEOUT*1000)
}
}
And this is my main code where I wait for the first function donglePaired, and if donglePaired return true I send an error to the user.
let donglePaired = await this.props.app.donglePaired();
if (donglePaired) return this.props.app.setError("ERR_DONGLE");
The problem is that the program doesnt wait for donglePaired, despite of the await
your code here is inappropriate
let donglePaired = await this.props.app.donglePaired();
if (donglePaired) return this.props.app.setError("ERR_DONGLE");
Async function cannot return value normally unless it is a Promise
See my simple demo below!
async function test() {
const result = await asyncRequest()
return result
}
function asyncRequest() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 2000)
})
}
test().then((data) => {
console.log(data)
})
The snippets should give you an idea how to await the callback
Sending to the API
async function remove_configuration(filename) {
const data = { filename };
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
};
await fetch('/delete', options);
}
Just Retrieving Data
async function display() {
let response = await fetch('/get-available-configurations')
let data = await response.json(); // JSON or Text what do you prefer
// do something with data
}
You could return an Promise.race() with your timeout function.
fetch(action, data, cb) {
let url = Config.URL + "?version=" + Config.VERSION + "&action=" + action;
let post = "";
let formData = new FormData();
for (let k in data) formData.append(k, data[k]);
for (let k in data)
post += "&" + k + "=" + encodeURIComponent(data[k]).replace(/%20/g, "+");
console.log(url + post);
console.log(url);
if (data.batch) console.log(data.batch);
let sending = true;
return Promise.race([
fetch(url, {
method: "post",
body: formData
})
.then(res => res.json())
.then(result => {
if (!sending) return;
sending = false;
return result;
}),
sleep(Config.HTTP_TIMEOUT * 1000)
]);
}
const sleep = ms => new Promise((_, rej) => setTimeout(rej("TIMEOUT"), ms));
It either returns you the value or it rejects with TIMEOUT or it rejects with an error from fetch
And donglePaired looks like this then. I have wrapped it with an try / catch
async donglePaired() {
if (Platform.OS !== "ios") {
var pairedDevices = await BluetoothScanner.getPairedDevices();
console.log("Sending........");
let data = {
data: pairedDevices
};
try {
let result = await new Api().fetch("bluetoothCheck", {
devices: JSON.stringify(data),
userid: this.state.probe.UID
});
if (!result.err) return false;
console.log("Dongle already paired");
return true;
//logNetworkState
} catch (err) {
console.log(err);
}
}
}
One possibility is to drop the async and change it to this:
donglePaired() {
return new Promise( function(resolve, reject) {
if (Platform.OS !=='ios'){
var pairedDevices = await BluetoothScanner.getPairedDevices();
console.log('Sending........');
let data={
data:pairedDevices,
};
new Api().fetch("bluetoothCheck",{devices:JSON.stringify(data),userid:this.state.probe.UID},(result) => {
if (!result.err) reject(false);
console.log("Dongle already paired");
resolve(true);
//logNetworkState
});
}
reject(false);
});
}
And:
this.props.app.donglePaired().then( (response) => {
// do something here, this will only run if the response is true
});

Custom fetch with promise fails

I want to implement fetch method by myself with promises with builder pattern, the backend is fine. I tested it with postman and it works fine. I don't know what could be, I tried everything but I don't get any data.
I think the problem is that it doesn't properly transform my data to JSON or something.
Any help will be appreciated.
class Fetch {
constructor(){
this.url = null;
this.result = null;
this.method = null;
this.header = null;
this.body = null;
}
call(){
return new Promise((resolve, reject) => {
fetch(this.url,
{method: this.method, header: this.header, body: this.body})
.then(response => response.json())
.then(data => {
this.result = data;
console.log(data);
resolve(data);
})
})
}
}
class FetchBuilder {
constructor(){
this.customFetch = new Fetch();
}
request(url){
this.flag = true;
this.customFetch.url = url;
return this;
}
method(method){
this.customFetch.method = method;
return this;
}
header(header){
this.customFetch.header = header;
return this;
}
body(body){
if(!this.flag){
this.customFetch.body = JSON.stringify(body);
}
return this;
}
query(obj){
}
send(){
this.customFetch.call();
}
}
const fetchObj = new FetchBuilder();
fetchObj.request('https://node-app-test-picsart.herokuapp.com/signin')
.method('POST')
.header({
'Content-Type': 'application/json;charset=utf-8'
})
.body({
email: 'bro#gmail.com',
password: 'bro'
})
.send()
Header:
Body
Your issue lies in either the this.flag = true or if (!this.flag) { line what causes the body argument passed in the .body() method to be lost. So you're code is doing what you want but just not in the right way, as you are expecting a package to be sent to the endpoint.
So either change the boolean:
...
request(url) {
this.flag = false;
this.customFetch.url = url;
return this;
}
...
Or the condition checking the boolean. The rest of your code works as expected.
...
body(body) {
if (this.flag) {
this.customFetch.body = JSON.stringify(body);
}
return this;
}
...
Run the code below with the network tab in your developer tools opened to see that a request is made with the body you want to send.
class Fetch {
constructor() {
this.url = null;
this.result = null;
this.method = null;
this.header = null;
this.body = null;
}
call() {
return fetch(this.url, {
method: this.method,
header: this.header,
body: this.body
})
.then(response => response.json())
.then(data => {
this.result = data;
console.log(data);
return data;
})
}
}
class FetchBuilder {
constructor() {
this.customFetch = new Fetch();
}
request(url) {
this.flag = true;
this.customFetch.url = url;
return this;
}
method(method) {
this.customFetch.method = method;
return this;
}
header(header) {
this.customFetch.header = header;
return this;
}
body(body) {
if (this.flag) {
this.customFetch.body = JSON.stringify(body);
}
return this;
}
query(obj) {}
send() {
this.customFetch.call();
}
}
const fetchObj = new FetchBuilder();
fetchObj.request('https://node-app-test-picsart.herokuapp.com/signin')
.method('POST')
.header({
'Content-Type': 'application/json;charset=utf-8'
})
.body({
email: 'bro#gmail.com',
password: 'bro'
})
.send()
In your Fetch().call() just return the fetch as it already returns a Promise, wrapping it like this would be an anti-pattern. You could also use the async / await syntax.

response.text is not a function while building react-native app

I'm trying to build a react-native app with expo and while trying to sign up I get the following error message stemming from my api.js file:
response.text is not a function. (In 'response.text()', 'response.text' is undefined).
Here is my code:
const BASE_URL = "my local IP:5000";
export const api = async (url, method, body = null, headers = {}) => {
try {
const endPoint = BASE_URL.concat(url);
const reqBody = body ? JSON.stringify(body) : null;
const fetchParams = {method, headers};
if((method === "POST" || method === "PUT") && !reqBody) {
throw new Error("Request body required");
}
if(reqBody) {
fetchParams.headers["Content-type"] = "application/json";
fetchParams.body = reqBody;
}
const fetchPromise = fetch(endPoint, fetchParams);
const timeOutPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject("Request Timeout");
}, 3000);
});
const response = await Promise.race([fetchPromise, timeOutPromise]);
return response;
} catch (e) {
return e;
}
}
export const fetchApi = async (url, method, body, statusCode, token = null, loader = false)
=> {
try {
const headers = {}
const result = {
token: null,
success: false,
responseBody: null
};
if(token) {
headers["x-auth"] = token;
}
const response = await api(url, method, body, headers);
console.log(response);
if(response.status === statusCode) {
result.success = true;
if(response.headers.get("x-auth")) {
result.token = response.headers.get("x-auth");
}
Here is response.text()
let responseBody;
const responseText = await response.text();
//const responseText = await response.json();
try {
responseBody = JSON.parse(responseText);
} catch (e) {
responseBody = responseText;
}
result.responseBody = responseBody;
return result;
}
Here is response.text()
let errorBody;
const errorText = await response.text();
//const errorText = await response.json();
try {
errorBody = JSON.parse(errorText);
} catch (e) {
errorBody = errorText;
}
result.responseBody = errorBody;
console.log(result);
throw result;
} catch (error) {
return error;
}
}
Any help would be immensely appreciated.

Categories

Resources