I am trying to send more than 1000 messages. The problem happens in the twilio catch: when an error returns, the loop breaks and does not continue to advance. How can I keep the loop running and thus ensure the sending of all messages?
try {
const targets: TargetSms[] = data.targets;
const rbd = data.rbd;
const idMessage = data.idMessage;
const messageRef = admin.firestore().collection(`/RBD/${rbd}/messages`).doc(idMessage);
await messageRef.set({ serverResponse: true }, { merge: true });
let countSeg = 0;
for (const target of targets) {
if (target.messagePayload) return target;
const textMessage = {
body: target.plainMsg,
to: target.targetNumber, // Text to this number
from: twilioNumber, // From a valid Twilio number
};
const payloadMessage = await client.messages
.create(textMessage)
.then(mess => mess)
.catch(err => {
console.warn('ocurrio un error al enviar mensaje', err)
target.messageError = err;
return null;
});
if(payloadMessage){
countSeg = countSeg + parseInt(payloadMessage.numSegments);
target.messagePayload = JSON.parse(JSON.stringify(new MessagePayload(payloadMessage)));
}
await admin.firestore().collection(`/RBD/${rbd}/targets`).doc(target.id).set(target);
await timeout(100);
};
await messageRef.set({totalSegments:countSeg},{merge:true});
await admin.firestore().doc(`/RBD/${rbd}`).set({config: {"countSms": admin.firestore.FieldValue.increment(countSeg)}},{merge:true});
return JSON.stringify({ suss: true, message: 'Mensajes enviados' })
} catch (error) {
return JSON.stringify({ suss: false, message: error })
}
You need to reformat your code a little bit, something like:
for (const target of targets) {
if (target.messagePayload) return target;
const textMessage = {
body: target.plainMsg,
to: target.targetNumber, // Text to this number
from: twilioNumber, // From a valid Twilio number
};
let payloadMessage;
try {
payloadMessage = await client.messages.create(textMessage);
} catch (err) {
console.warn('ocurrio un error al enviar mensaje', err)
target.messageError = err;
// continue; // Remove this line to continue the execution of finally block
} finally {
if (payloadMessage) {
countSeg = countSeg + parseInt(payloadMessage.numSegments);
target.messagePayload = JSON.parse(JSON.stringify(new MessagePayload(payloadMessage)));
}
await admin.firestore().collection(`/targets`).doc(target.id).set(target);
await timeout(100);
}
};
In this way you can get rid of the "old" then syntax, because async - await lets you to put your await code inside a try - catch block to intercept the errors from the await call, and everything else after will be executed in case of success.
Also use continue to instead of return null.
Maybe you can get rid of the conditional block if (payloadMessage) because try - catch ensures that your variable has a value, but idk for sure.
Specs here : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
Use the continue statement to "continue" processing items after an error in your loop.
continue
Related
My function works for the first call only; meaning I only get 50 video IDs and then it throw a lot of errors (400 error). And then when I use the nextPageToken, it doesn’t work. I believe it’s because I’m calling the API inside a while loop, and I end up with an infinite loop.
Here's my current code:
const DEFAULT_CHANNEL_UPLOADS_ID = "UUJQJAI7IjbLcpsjWdSzYz0Q";
async function getPlaylistItemsById(
id = DEFAULT_CHANNEL_UPLOADS_ID,
nextPageToken = undefined
) {
const YOUTUBE_CHANNEL_UPLOADS = [];
try {
const request = await gapi.client.youtube.playlistItems.list({
part: ["snippet, contentDetails"],
maxResults: 50,
playlistId: id,
nextToken: nextPageToken,
});
const response = await request.result;
for (const item of response.items) {
YOUTUBE_CHANNEL_UPLOADS.push(item.contentDetails.videoId);
}
nextPageToken = response?.nextPageToken;
if(nextPageToken === undefined) return;
while (nextPageToken !== undefined) {
getPlaylistItemsById(DEFAULT_CHANNEL_UPLOADS_ID, nextPageToken);
}
return YOUTUBE_CHANNEL_UPLOADS;
} catch (error) {
console.log(error);
}
}
I made a few mistakes in my previous implementation, and luckily I managed to fix them.
Here's what I fixed:
I used pageToken instead of nextToken.
I moved YOUTUBE_CHANNEL_UPLOADS array to the global scope.
Used an if statement to return in case nextPageToken is equal to undefined.
Otherwise, call the function recursively and pass the id alongside nextPageToken.
const DEFAULT_CHANNEL_UPLOADS_ID = "UUJQJAI7IjbLcpsjWdSzYz0Q";
const YOUTUBE_CHANNEL_UPLOADS = [];
async function getPlaylistItemsById(
id = DEFAULT_CHANNEL_UPLOADS_ID,
nextPageToken = undefined
) {
try {
const request = await gapi.client.youtube.playlistItems.list({
part: ["snippet, contentDetails"],
maxResults: 50,
playlistId: id,
pageToken: nextPageToken,
});
const response = await request.result;
for (const item of response.items) {
YOUTUBE_CHANNEL_UPLOADS.push(item.contentDetails.videoId);
}
nextPageToken = response.nextPageToken;
if (nextPageToken === undefined) return YOUTUBE_CHANNEL_UPLOADS;
return await getPlaylistItemsById(id, nextPageToken);
} catch (error) {
console.log(error);
}
}
well, in the code you can see that when the first object is saved, the id comes out undefined and then the other objects start to come out fine, I was thinking about it but I can't fix it, the problem is in the save() function in the part that pushes the newProduct does anyone realize what the problem is?
const fs = require("fs");
class Container {
constructor(fileName) {
this.fileName = fileName;
}
async createEmptyFile() {
fs.writeFile(this.fileName, "[]", (error) => {
if (error) {
console.log(error)
} else {
console.log(`File ${this.fileName} was created`);
}
});
}
async readFile() {
try {
const data = await fs.promises.readFile(this.fileName, 'utf-8');
return JSON.parse(data);
} catch (error) {
if (error) {
this.createEmptyFile();
} else {
console.log(`Error Code: ${error} | There was an unexpected error when trying to read ${this.fileName}`);
}
}
}
async save(title, price, thumbnail) {
try {
const data = await this.readFile();
const newProduct = {
title,
price,
thumbnail,
id: data.length + 1
}
data.push(newProduct);
await fs.promises.writeFile(this.fileName, JSON.stringify(data));
return newProduct.id;
} catch (error) {
console.log(`Error Code: ${error.code} | There was an error when trying to save an element`);
}
}
}
const container = new Container("products.json");
const main = async () => {
const id1 = await container.save(
"Regla",
75,
"https://rfmayorista.com.ar/wp-content/uploads/2020/03/REGLA-ECONM_15-CM.-600x600.jpg"
);
const id2 = await container.save(
"Goma",
50,
"https://image.shutterstock.com/image-photo/rubber-eraser-pencil-ink-pen-260nw-656520052.jpg"
);
const id3 = await container.save(
"Lapicera",
100,
"https://aldina.com.ar/wp-content/uploads/2020/08/bic-cristal-trazo-fino-azul-1.jpg"
);
console.log(id1, id2, id3);
};
main();
From already thank you very much
In the readFile method you need to return something on the catch statement in order to get consistent return values
async readFile() {
try {
const data = await fs.promises.readFile(this.fileName, 'utf-8');
return JSON.parse(data);
} catch (error) {
if (error) {
this.createEmptyFile();
} else {
console.log(`Error Code: ${error} | There was an unexpected error when trying to read ${this.fileName}`);
}
// returns [] because this seems to be the default value for the file in createEmptyFile
return []; // Add this line
}
}
What happens is practice is: the file doesn't exist, the try statement fails, then the catch statement and the function don't return anything thus undefined.
Please help me figure out the difference in return behaviour between the onCall and onRequest google functions below.
onCall, the problem: returns null on all returns, except at the first return (as commented below). The db entries and rest of the code works fine. Just no returns problem.
onRequest, returns perfectly fine on every return. The db entries and rest of the code also works fine.
Both as you will see compare the same, but I just can't seem to get it to work at all. Any advice on how to get my returns to work for the onCall (and structure it better) would be much appreciated.
I am keen on sticking with async await (as opposed to a promise). Using Node.js 12. I am calling the onCall in Flutter, don't know if that is relevant for the question.
The onCall:
exports.applyUserDiscount = functions.https.onCall(async (data, context) => {
if (!context.auth) return {message: "Authentication Required!", code: 401};
const uid = context.auth.uid;
const discountCode = data["discountCode"];
const cartTotal = data["cartTotal"];
try {
return await db.collection("discountCodes").where("itemID", "==", discountCode).limit(1).get()
.then(async (snapshot) => {
if (snapshot.empty) {
return "doesNotExist"; // The only return that works.
} else { // Everything else from here onwards returns null.
snapshot.forEach(async (doc) => {
if (doc.data().redeemed == true) {
return "codeUsed";
} else {
const newCartTotal = cartTotal - doc.data().discountAmount;
if (newCartTotal < 0) {
return "lessThanTotal";
} else {
doc.ref.update({
redeemed: true,
uid: uid,
redeemDate: fireDateTimeNow,
});
await db.collection("userdata").doc(uid).set({
cartDiscount: admin.firestore.FieldValue.increment(-doc.data().discountAmount),
}, {merge: true});
return doc.data().discountAmount.toString();
}
}
});
}
});
} catch (error) {
console.log("Error:" + error);
return "error";
}
});
The onRequest:
exports.applyUserDiscount = functions.https.onRequest(async (req, res) => {
const uid = req.body.uid;
const discountCode = req.body.discountCode;
const cartTotal = req.body.cartTotal;
try {
return await db.collection("discountCodes").where("itemID", "==", discountCode).limit(1).get()
.then(async (snapshot) => {
if (snapshot.isempty) {
res.send("doesNotExist");
} else {
snapshot.forEach(async (doc) => {
if (doc.data().redeemed == true) {
res.send("codeUsed");
} else {
const newCartTotal = cartTotal - doc.data().discountAmount;
if (newCartTotal < 0) {
res.send("lessThanTotal");
} else {
doc.ref.update({
redeemed: true,
uid: uid,
redeemDate: fireDateTimeNow,
});
await db.collection("userdata").doc(uid).set({
cartDiscount: admin.firestore.FieldValue.increment(-doc.data().discountAmount),
}, {merge: true});
res.send(doc.data().discountAmount.toString());
}
}
});
}
});
} catch (error) {
console.log(error);
res.send("error");
}
});
There are several points to be noted when looking at your code(s):
You should not use async/await within a forEach loop. The problem is that the callback passed to forEach() is not being awaited, see more explanations here or here. HOWEVER, in your case you don't need to loop over the QuerySnapshot since it contains only one doc. You can use the docs property which return an array of all the documents in the QuerySnapshot and take the first (and unique) element.
You mix-up then() with async/await, which is not recommended.
I would advise to throw exceptions for the "error" cases, like doesNotExist, codeUsed or lessThanTotal but it's up to you to choose. The fact that, for example, the lessThanTotal case is an error or a standard business case is debatable... So if you prefer to send a "text" response, I would advise to encapsulate this response in a Object with one property: in your front-end the response will always have the same format.
So, the following should do the trick. Note that I send back on object with a response element, including for the cases that could be considered as errors. As said above you could throw an exception in these cases.
exports.applyUserDiscount = functions.https.onCall(async (data, context) => {
if (!context.auth) ... //See https://firebase.google.com/docs/functions/callable#handle_errors
const uid = context.auth.uid;
const discountCode = data["discountCode"];
const cartTotal = data["cartTotal"];
try {
const snapshot = await db.collection("discountCodes").where("itemID", "==", discountCode).limit(1).get();
if (snapshot.empty) {
//See https://firebase.google.com/docs/functions/callable#handle_errors
} else {
const uniqueDoc = snapshot.docs[0];
if (uniqueDoc.data().redeemed == true) {
return { response: "codeUsed" };
} else {
const newCartTotal = cartTotal - uniqueDoc.data().discountAmount;
if (newCartTotal < 0) {
return { response: "lessThanTotal" };
} else {
await uniqueDoc.ref.update({ // See await here!!
redeemed: true,
uid: uid,
redeemDate: fireDateTimeNow,
});
await db.collection("userdata").doc(uid).set({
cartDiscount: admin.firestore.FieldValue.increment(-uniqueDoc.data().discountAmount),
}, { merge: true });
return {
response: uniqueDoc.data().discountAmount.toString()
}
}
}
}
} catch (error) {
console.log("Error:" + error);
return "error";
}
});
I have a simple "then chain" that runs some functinality steps. If some condition is met I need to cancel the chain and exit (mainly if an error occurrs). I'm using this in a firebase cloud function, but I think it is a basic concept applicable to any node/express eviroment.
This is the code I have:
let p1 = db.collection('products').doc(productId).get();
let p2 = db.collection('user_data').doc(userUid).get();
let promises= [p1,p2];
return Promise.all(promises)
.then(values =>
{
let proudctDoc = values[0];
let userDataDoc = values[1];
if(!proudctDoc.exists)
{
console.log("The product does not exist");
response.status(404).json({error: ErrorCodes.UNKNOWN_PRODUCT, msg: "The products does not exist"});
throw("CANCEL");
}
if(!userDataDoc.exists)
{
console.log("User data block not found!");
response.status(404).json({error: ErrorCodes.UNKNOWN_USER_DATA, msg: "User data block not found!"});
throw("CANCEL");
}
variantCountryRef = db.doc('products/'+productId+'/variants/'+variantCountry);
return variantCountryRef.get();
})
.then(variantCountryDoc =>
{
....
})
.catch(err =>
{
if(err !== "CANCEL")
{
//Deal with real error
}
}
As you see, I just run 2 promises and wait for them to finish. After this I check some returned values and notify the client if an error occurs. At this time I must finish the "Then chain".
Is this a common pattern? Anything I could improve?
You could do as follows:
const p1 = db.collection('products').doc(productId).get();
const p2 = db.collection('user_data').doc(userUid).get();
const promises = [p1, p2];
return Promise.all(promises)
.then(values => {
let proudctDoc = values[0];
let userDataDoc = values[1];
if (!proudctDoc.exists) {
console.log('The product does not exist');
throw new Error(ErrorCodes.UNKNOWN_PRODUCT);
}
if (!userDataDoc.exists) {
console.log('User data block not found!');
throw new Error(ErrorCodes.UNKNOWN_USER_DATA);
}
variantCountryRef = db.doc(
'products/' + productId + '/variants/' + variantCountry
);
return variantCountryRef.get();
})
.then(variantCountryDoc => {
//Don't forget to send a response back to the client, see https://www.youtube.com/watch?v=7IkUgCLr5oA&
//...
//response.send(...);
})
.catch(err => {
if (err.message === ErrorCodes.UNKNOWN_PRODUCT) {
response.status(404).json({
error: ErrorCodes.UNKNOWN_PRODUCT,
msg: 'The products does not exist'
});
} else if (err.message === ErrorCodes.UNKNOWN_USER_DATA) {
response
.status(404)
.json({
error: ErrorCodes.UNKNOWN_USER_DATA,
msg: 'User data block not found!'
});
} else {
//Deal with other error types
}
});
Please, I don't have much of idea of how to go about this. A good explanation of the process will be of great help. Thanks in anticipation.
Here is my controller
async getAllEntries(req, res) {
try {
const userId = req.userData.userID;
const query = await client.query(
`SELECT * FROM entries WHERE user_id=($1) ORDER BY entry_id ASC;`, [
userId,
],
);
const entries = query.rows;
const count = entries.length;
if (count === 0) {
return res.status(200).json({
message: 'There\'s no entry to display',
});
}
return res.status(200).json({
message: "List of all entries",
"Number of entries added": count,
entries,
});
} catch (error) {
return res.status(500).json({
message: "Error processing request",
error,
});
}
}
For this case, what I'm going to do is to make the client.query process failed. So, based on your code, it will go to catch statement.
const chai = require('chai');
const assert = chai.assert;
const sinon = require('sinon');
const client = require('...'); // path to your client library
const controller = require('...'); // path to your controller file
describe('controller test', function() {
let req;
let res;
// error object to be used in rejection of `client.query`
const error = new Error('something weird');
beforeEach(function() {
req = sinon.spy();
// we need to use `stub` for status because it has chain method subsequently
// and for `json` we just need to spy it
res = {
status: sinon.stub().returnsThis(),
json: sinon.spy()
};
// here we reject the query with specified error
sinon.stub(client, 'query').rejects(error);
});
afterEach(function() {
sinon.restore();
})
it('catches error', async function() {
await controller.getAllEntries(req, res);
// checking if `res` is called properly
assert(res.status.calledWith(500));
assert(res.json.calledWith({
message: 'Error processing request',
error
}));
});
});
Hope it helps.