After post request, continue checking for completion | Node.js - javascript

I'm making a post request with a good amount of data that will take about a minute to finish saving. The hosting service I'm using for this will time out requests after 5 seconds, so I need to set this up to periodically check if the data saving is complete to give a final update.
I'm using request-promise, and have looked at both setTimeout and setInterval approaches. In my latest attempt (below) I'm using a setTimeout approach, but my second then keeps being called pretty much immediately. I want this to hang out in the first then stage until it's checked a bunch of times (24 here) or actually finished.
I might have a totally wrong approach here, but I'm not finding examples of the thing I'm trying to reference. Any direction to a good example of this or where I'm going wrong would be greatly appreciated.
const request = require('request-promise');
function checkFiles () {
return request({
uri: `${process.env.ROOT_URL}/api/v1/get/file-processing`,
method: 'GET',
json: true
})
.then(res => { return res; })
.catch(err => { return err; });
}
async function init () {
const filesPostOptions = {/* request options */};
await request(filesPostOptions)
.then(async status => { // THEN #1
if (status.status === 201) {
return status;
}
let checks = 0;
const checkIt = function() {
checks++;
checkFiles()
.then(res => {
if (res.status === 201 || checks > 24) {
status = res;
return status;
} else {
setTimeout(checkIt, 5000);
}
})
.catch(err => {
console.error(err);
});
};
checkIt();
})
.then(status => { // THEN #2
if (!status.status) {
throw Error('Post request timed out.');
}
return status;
})
.catch(err => {
err = err.error ? err.error : err;
console.error(err);
});
}
The post response will deliver a response with a status property (the status code) and a message property.

You need to control the return in "THEN #" by adding a Promise:
.then(async status => { // THEN #1
return new Promise((resolve, reject) => { // <---- prevent an immediate return
if (status.status === 201) {
return resolve(status);
}
let checks = 0;
const checkIt = function() {
checks++;
checkFiles()
.then(res => {
if (res.status === 201 || checks > 24) {
status = res;
resolve(status);
} else {
setTimeout(checkIt, 1000);
}
})
.catch(err => reject(err));
};
checkIt();
})
})

Related

Axios interceptors runs more than once

I have this interceptors that logs the user out if unauthorized. I do, however, get more than a 401 response, and the interceptors does therefore run for as many times as the responses I get (4). Is there a way to make it run only for the first one?
This is my code:
api.interceptors.response.use(
(response) => response,
(err) => {
if (err.response.status === 401 && isLoggedIn) {
api
.delete("auth/sign_out")
.then((resp) => {
clearLocalStorage();
})
.catch((err) => {
clearLocalStorage();
});
} else {
return Promise.reject(err);
}
return err;
}
);
You might want something like this to "lock out" the possible re-entrant calls:
let isLoggingOut = false; // global
// ...
api.interceptors.response.use(
(response) => response,
async (err) => {
if (err.response.status === 401 && isLoggedIn) {
if(!isLoggingOut) {
isLoggingOut = true; // disallow re-entrant calls
try {
await api.delete('auth/sign_out');
} catch (deletionError) {
// throw errors away
} finally {
clearLocalStorage();
isLoggingOut = false;
isLoggedIn = false; // if the variable is assignable
}
}
}
return err;
},
);

Code works from project but not from a Firestore Cloud Function

Update: Refactored the code at the end below to this:
let getMembersIDs = eventID => {
return admin.firestore().collection('events').doc(eventID).get()
.then(doc => {
if (doc.exists) {
console.log(doc.data().members)
return doc.data().members
}
else throw new Error ('Event does not exist!', doc.id)
})
}
let dispatchToMembers = (arrayMembersIDs, data) => {
return Promise.all(
arrayMembersIDs.map(memberID => {
if (data.at===memberID) data.type = 'pm'
else data.content = 'New message in an event chat you participate to.'
console.log('Sending now to: ', memberID)
return admin.firestore().collection('users').doc(memberID).collection('inbox').add({
content: 'You were mentioned in a recent chat: ' + data.content,
type: data.type
})
})
)
}
getMembersIDs(data.target).then(members => dispatchToMembers(members, data)).then(() => {
console.log('Message dispatched!')
res.end()
return true
})
.catch(err => {
console.error(err);
response.status(500).send(err);
return true;
})
})
It does work when I run it from my project, replacing admin.firestore() with db. However when embedded within a cloud function it does not work: the cloud function returns codes 204 and 200 but the desired db operation does not seem to occur. Pulling my hair as I don't understand why.
Outdate: I am not able to debug the following piece of code. A simple cloud function with two parts (a read, a write) chained with promises.
exports.discuss = functions.https.onCall((data, context) => {
return admin.firestore().collection('events').doc(data.target).get()
.then(doc => {
if (doc.exists) return doc.data().members
else throw new Error ('Doc does not exist ', doc.id)
})
.then(members => {
let promises = []
members.forEach(u => {
...
let promise = new Promise(res => {
admin.firestore().collection('users').doc(u).collection('inbox').add({...})
res(u)
})
promises.push(promise)
})
return Promise.all(promises)
})
.then(() => {
response.send('ok')
return;
})
.catch(err, response.status(500).send(err))
})
You generate that 500 code with this line:
.catch(err, response.status(500).send(err))
The client that invokes the Cloud Function should be getting the error message in the response body. But you'll also want to log it on the server with:
.catch(function(err) {
console.error(err);
response.status(500).send(err);
return true;
})

How to remove the following promise catches without making the code hang?

The following code loops through some form fields. If the field is a file that has to be uploaded it runs an api.uploadPhotofunction (setting the payload once the photos has been uploaded). If the field is a normal input when the payload is set directly:
formFields.forEach(field => {
if (hasUploadFiles(field)) {
uploadPhotoPromise = new Promise((resolve, reject) => {
uploads.queued.push(file)
api.uploadPhoto(file, field).then(uploadedPhoto => {
uploads.finished.push(field)
if (uploads.queued.length === uploads.finished.length) {
payload[field.name] = uploadedPhoto
resolve()
} else {
reject()
}
}).catch(error => {
console.log('error:', error)
reject()
})
}).catch(error => {
console.log('error:', error)
})
} else {
payload[field.name] = field.value
}
})
Promise.all([uploadPhotoPromise]).then(values => {
// update action
}
The code works. However, all those catch make it look a bit messy.
I tried removed them but the code hangs if I remove any of them (the code inside Promise.all never runs). Why is this? And how to refactor this code without all those catch statements without making the it hang?
Original code (plus Bergi's suggested modification):
const buildingFormPromise = utils.mapDeep(this.buildingForm.schema, field => {
if (!field.name) return // fields not in the database
else if (utils.hasUploadFiles(field)) {
utils.eachCall(field.value, (file, index) => {
field.userId = this.user.id
this.uploads.queued.push(file)
this.$set(this.uploads.queued, index, { progress: 30 })
return api.uploadPhoto(file, field).then(uploadedPhoto => {
this.$set(this.uploads.queued, index, { progress: 100 })
return loadImage(uploadedPhoto, () => {
this.uploads.finished.push(field)
if (this.uploads.queued.length === this.uploads.finished.length) {
console.log('This runs after the code inside Promise.all')
buildingPayload[field.name] = uploadedPhoto
}
})
})
})
} else {
return Promise.resolve(buildingPayload[field.name] = field.value)
}
})
Promise.all([buildingFormPromise]).then(values => {
console.log('This runs before the files are uploaded')
})
You need to pass an array of all the promises into Promise.all, and you should avoid the Promise constructor antipattern. You can move the .catch to the very end if you don't want to handle individual upload failures.
var fieldValuePromises = formFields.map(field => {
if (hasUploadFiles(field)) {
return api.uploadPhoto(file, field).then(uploadedPhoto => {
return payload[field.name] = uploadedPhoto;
});
} else {
return Promise.resolve(payload[field.name] = field.value);
}
});
Promise.all(fieldValuePromises).then(values => {
// update action
}).catch(error => {
// at least one upload failed
console.log('error:', error)
});

ApolloClient timeout best option

I came up with this way of doing network operations with ApolloClient but the problem is that the code looks very ugly and difficult to read, considering I have to write dozens of queries like this, it becomes tiresome and unmaintainable.
I haven't found anything in the Apollo docs or the actual code to configure the timeout.
let query = gql`
query ... {
}`;
let x = 0;
let timer = setTimeout(() => {
if (x === 0) {
console.log('error');
}
x = 1;
}, 3000);
ApolloClient.query({ query }).then(({data}) => {
clearTimeout(timer);
if (x === 0) {
if (data.result) {
console.log(data.result)
} else {
console.log('error');
}
}
}).catch((error) => {
clearTimeout(timer);
console.log('error')
});
Is there a better way of achieving the same result with less and simpler code?
Turns out, you can override the method:
export async function query(request) {
const options = {...this._opts};
return new Promise((resolve, reject) => {
setTimeout(() => reject('Network timed out'), 1e4); // 10 sec
return this.applyMiddlewares({
request,
options
}).then((rao) => {
return this.fetchFromRemoteEndpoint.call(this, rao);
}).then(response => this.applyAfterwares({
response: response,
options
})).then(({response}) => (response).json()).then((payload) => {
resolve(payload);
}).catch((e) => {
reject(e);
});
}).catch((e) => {
console.log(e);
return null;
});
}

How to retrieve all posts of a user via Facebook Graph API using promises and recursion?

I am currently developing a web app which uses the Facebook Graph API.
What I would like to achieve is to get all posts of a user.
However, this is not that easy since I have to paginate the results.
At the moment I am struggeling with promises.
What I try to achieve is to fill an array with the post objects.
Therefore I use promises and recursion which does not work as expected.
My code currently looks as follows:
// Here I retrieve the user with his or her posts,
// just the first 25 due to pagination
if (accessToken) {
return new Promise(resolve => {
FB.api('/me?fields=id,name,posts&access_token=' + accessToken, response => {
this.get('currentUser').set('content', response);
resolve()
})
})
}
// Returns all posts of a given user
function getAllPostsOfUser(posts, postsArr) {
// Process each post of the current pagination level
for (var post of posts.data) {
// Only store meaningful posts
if (post !== undefined && post.message !== undefined) {
postsArr.push(post)
}
}
// Further posts are retrievalable via paging.next which is an url
if (posts.data.length !== 0 && posts.paging.next !== undefined) {
FB.api(posts.paging.next, response => {
getAllPostsOfUser(response, postsArr)
resolve()
})
}
return postsArr
}
var posts = getAllPostsOfUser(this.get('currentUser').content.posts, [])
// I want to use all the posts here
console.log(posts)
The problem I have is that I want to use the posts where the console.log is placed but when I log the posts array a lot of posts are missing.
I am sure that I did something wrong with the promises but I do not know what.
I would be glad if anyone could guide me to a solution.
Thank you in advance.
Try this:
function getAllPosts() {
return new Promise((resolve, reject) => {
let postsArr = [];
function recursiveAPICall(apiURL) {
FB.api(apiURL, (response) => {
if (response && response.data) {
//add response to posts array (merge arrays), check if there is more data via paging
postsArr = postsArr.concat(response.data);
if (response.paging && response.paging.next) {
recursiveAPICall(response.paging.next);
} else {
resolve(postsArr);
}
} else {
reject();
}
});
}
recursiveAPICall("/me/posts?fields=message&limit=100");
});
}
getAllPosts()
.then((response) => {
console.log(response);
})
.catch((e) => {
console.log(e);
});
Not tested, just a quick example I came up with. It returns a promise and uses a recursive function to get all entries. BTW, you don't need to add the Access Token. If you are logged in, the SDK will use it internally.
This is an old question that is already answered but I thought it could use a more modern answer, considering how many lines of code could be saved. This code has not been tested with the real API but it should work.
This function returns a promise of an array of posts.
async function getPosts(url = "/me/posts?fields=message&limit=100") {
const { error, paging, data } = (await new Promise(r => FB.api(url, r))) || {}
if (error || !data) throw new Error(error || "Could not get posts")
return data.concat(paging?.next ? await getPosts(paging.next) : [])
}
With comments:
async function getPosts(url = "/me/posts?fields=message&limit=100") {
// get response data out of callback
const { error, paging, data } = (await new Promise(r => FB.api(url, r))) || {}
// if there was an error or there wasn't any data, throw
if (error || !data) throw new Error(error || "Could not get posts")
// return this page's data + if there's a next page, recursively get its data
return data.concat(paging?.next ? await getPosts(paging.next) : [])
}
The function can then be consumed like so:
async function main() {
try {
const posts = await getPosts(); // the array of posts
console.log(posts);
} catch (error) {
console.error(error);
}
}
main();
Below is a snippet demonstrating the function using a fake API.
// fake api for testing purposes
const FB = {
api(url, callback) {
const pages = [
{
data: ["post1", "post2", "post3"],
paging: { next: 1 },
},
{
data: ["post4", "post5", "post6"],
paging: { next: 2 },
},
{
data: ["post7", "post8", "post9"],
},
];
if (typeof url !== "number") return callback(pages[0]);
return callback(pages[url]);
},
};
async function getPosts(url = "/me/posts?fields=message&limit=100") {
const { error, paging, data } = (await new Promise(r => FB.api(url, r))) || {}
if (error || !data) throw new Error(error || "Could not get posts")
return data.concat(paging?.next ? await getPosts(paging.next) : [])
}
async function main() {
try {
const posts = await getPosts(); // the array of posts
console.log(posts);
} catch (error) {
console.error(error);
}
}
main();
.as-console-wrapper{max-height:none !important;top: 0;}
Also see erikhagreis's ESM wrapper on GitHub.

Categories