Is this a good way of breaking "THEN chain" when using promises? - javascript

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
}
});

Related

web-bluetooth NotSupportedError: GATT operation not permitted

I am getting error code "NotSupportedError: GATT operation not permitted."
I am trying to connect to ESP32 bluetooth. I tested using nRF Connect and messages are getting to the hardware. Next I tried to use javascript and web-bluetooth. Unfortunately I am getting error on console.error('Argh! ' + error) line of code. The error is happening on characteristic.writeValue()
The code was run on https.
The code below
$(document).ready(function(){
let bluetoothDevice = null;
let requestDeviceParams = {
filters: [
{name: ["konko"]}
],
optionalServices: ['10001001-0000-1000-8000-00805f9b34fb']
}
let name = document.querySelector('#date-input')
$("#synch-date-time").click(() => {
if (document.querySelector('#date-input').value === '') {
console.error('empty date field, please fill the fields')
return
}
asyncResultNotif();
})
async function asyncResultNotif(){
return await navigator.bluetooth.requestDevice(requestDeviceParams)
.then(device => {
bluetoothDevice = device.gatt;
return device.gatt.connect();
})
.then(server => {
if (bluetoothDevice.connected) {
return server.getPrimaryService('10001001-0000-1000-8000-00805f9b34fb');
} else {
console.log('cant connect to prime server')
}
})
.then(service => {
if (bluetoothDevice.connected) {
return service.getCharacteristic('10001111-0000-1000-8000-00805f9b34fb'); // write one value
} else {
console.log('cant connect to characteristic')
}
})
.then(characteristic => {
if (bluetoothDevice.connected) {
// const resetEnergyExpended = Uint8Array.of(1);
// return characteristic.writeValue(resetEnergyExpended);
let data = '{"ssid": "' +name.value
data +='"}'
console.log(data)
let encoder = new TextEncoder('utf-8');
let val = encoder.encode(data);
// return characteristic.writeValue(val.buffer)
return characteristic.writeValue(new Uint8Array([1]))
} else {
console.log('cant send message over BLE')
}
}). then(() => {
if (bluetoothDevice.connected) {
bluetoothDevice.disconnect();
} else {
console.log('> Bluetooth Device is already disconnected');
}
}).catch(error => {
console.error('Argh! ' + error)
});
}
It could be that the Bluetooth GATT characteristic you're writing to is not writable. Can you share your ESP32 server code as well?
It should look something like below. Note that I use BLECharacteristic::PROPERTY_WRITE.
class MyCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
std::string value = pCharacteristic->getValue();
BLECharacteristic *pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
pCharacteristic->setCallbacks(new MyCallbacks());
pCharacteristic->setValue("Hello World");
I found the answer. The problem was with UUID of characteristic. On ESP32 it was "10001111-0000-1000-8000-00805f9b34fb" and when I changed that to "10000000-0000-1000-8000-00805f9b34fb" it starts working. I somewhere read that it may be something with 128bits in UUID. We can mark with as answered.

onRequest vs onCall returning null

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";
}
});

Nodejs Firebase Transaction - Maximum call stack size exceeded

I have a cloud function that uses a transaction to updates the players in a game. When the /players is null, i am trying to return a Map, but i get "Maximum call stack size exceeded".
Here is my cloud function:
export const addUserToGame = functions.https.onCall((data, context) => {
// Expected inputs - game_id(from data) and UID(from context)
if (context.auth == null) {
return {
"status": 403,
"message": "You are not authorized to access this feature"
};
}
const uid = context.auth.uid;
const game_id = data.game_id;
let gameIDRef = gamesRef.child(game_id);
return gameIDRef.once("value", function (snapshot) {
let players: Map<String, Number> = snapshot.child("players").val();
let max_players: Number = snapshot.child("max_players").val();
if (players != null && players.has(uid)) {
return {
"status": 403,
"message": "Player already in the game"
}
} else if (players != null && players.size >= max_players) {
return {
"status": 403,
"message": "Game is already full"
}
} else {
let playersNodeRef = gamesRef.child(game_id).child("players");
return playersNodeRef.transaction(t => {
if (t === null) {
return new Map<String, Number>().set(uid, 1);//trying to set a map with the player data, when the /players is null
} else {
let playersData: Map<String, Number> = t;
if (playersData.size >= max_players) { // rechecking
return;
} else {
playersData.set(uid, 1);
return playersData;
}
}
}).then(result => {
if (result.committed) { // if true there is a commit and the transaction went through
return {
"status": 200,
"message": "User added to game successfully"
}
} else {
return {
"status": 403,
"message": "Unable to add user at this time. Please try again"
}
}
}).catch(error => {
return {
"status": 403,
"message": error
}
});
}
});
});
Here is the stack trace:
addUserToGame
Function execution took 1423 ms, finished with status code: 500
at /workspace/node_modules/lodash/lodash.js:13401:38
at encode (/workspace/node_modules/firebase-functions/lib/providers/https.js:179:18)
at Function.mapValues (/workspace/node_modules/lodash/lodash.js:13400:7)
at baseForOwn (/workspace/node_modules/lodash/lodash.js:2990:24)
at /workspace/node_modules/lodash/lodash.js:4900:21
at keys (/workspace/node_modules/lodash/lodash.js:13307:14)
at isArrayLike (/workspace/node_modules/lodash/lodash.js:11333:58)
at isFunction (/workspace/node_modules/lodash/lodash.js:11653:17)
at baseGetTag (/workspace/node_modules/lodash/lodash.js:3067:51)
at Object (<anonymous>)
Unhandled error RangeError: Maximum call stack size exceeded
How can i set a map to /players node?
There were more than one issues with the code and as #Renaud pointed out, i have changed the 'once' callback to use promises version.
Also i had issues sending back data in the transaction. The data that i sent was using complex JS objects like Map(), but after some struggle (with the syntax) i changed it to a normal JS object (json like structure). Please see my changes below:
if (t === null) {
return [{ [uid]: { "status": 1 } }]; // if null, create an array and add an object to it
} else {
let playersData = t;
if (playersData.size >= max_players) { // rechecking
return;
} else { // if not null create an object and add to the existing array
playersData.push({
[uid]: {
"status": 1
}
});
return playersData;
}
}
Your problem most probably comes from the fact you are returning a complex JavaScript object, see https://stackoverflow.com/a/52569728/3371862.
In addition, note that you should use the promise version of the once() method, since, in a Callable Cloud Function you must return a promise that resolves with the data object to send back to the client.
Instead of doing
return gameIDRef.once("value", function (snapshot) {...});
do
return gameIDRef.once("value").then(snapshot => {...});
With this you will be able to correctly build the promise chain to be returned. Also, when dealing with the different cases around the players value, instead of returning JavaScript objects that will be handle in the .then((result) => {...}) block (which is not necessary and not really logical), throw errors that will be handled in the catch() block.
Something along the following lines:
export const addUserToGame = functions.https.onCall((data, context) => {
// Expected inputs - game_id(from data) and UID(from context)
if (context.auth == null) {
return {
status: 403,
message: 'You are not authorized to access this feature',
};
// IMHO better to do throw new functions.https.HttpsError('...', ...);
}
const uid = context.auth.uid;
const game_id = data.game_id;
let gameIDRef = gamesRef.child(game_id);
return gameIDRef
.once('value')
.then((snapshot) => {
let players: Map<String, Number> = snapshot.child('players').val();
let max_players: Number = snapshot.child('max_players').val();
if (players != null && players.has(uid)) {
throw new Error('Player already in the game');
} else if (players != null && players.size >= max_players) {
throw new Error('Game is already full');
} else {
let playersNodeRef = gamesRef.child(game_id).child('players');
return playersNodeRef.transaction((t) => {
if (t === null) {
return new Map<String, Number>().set(uid, 1); //trying to set a map with the player data, when the /players is null
} else {
let playersData: Map<String, Number> = t;
if (playersData.size >= max_players) {
// rechecking
return;
} else {
playersData.set(uid, 1);
return playersData;
}
}
});
}
})
.then((result) => {
if (result.committed) {
// if true there is a commit and the transaction went through
return {
status: 200,
message: 'User added to game successfully',
};
} else {
// probably throw an error here
return {
status: 403,
message: 'Unable to add user at this time. Please try again',
};
}
})
.catch((error) => {
if (error.message === 'Player already in the game') {
throw new functions.https.HttpsError('...', error.message);
} else if (error.message === 'Game is already full') {
throw new functions.https.HttpsError('...', error.message);
} else {
throw new functions.https.HttpsError('internal', error.message);
}
});
});
See here for more details on how to handle errors in a Callable Cloud Function.

Stripe functions returning calls after function finished

Relatively new to Javascript however, i'm trying to work with Stripe and my way around a user submitting another payment method and then paying an invoice with that method. if the payment fails again - it should remove the subscription alltogether. I'm using Firebase Realtime Database with GCF & Node.js 8.
Here is what i have so far
exports.onSecondPaymentAttempt = functions.database.ref("users/{userId}/something/somethingHistory/{subDbId}/newPayment").onCreate((snapshot, context)=>{
var s = snapshot.val();
var fields = s.split(",");
const cardToken = fields[0];
const cus_id = fields[1];
const conn_id = fields[2];
const subDbId = context.params.subDbId;
const userId = context.params.userId;
return stripe.customers.createSource(
cus_id,
{source: cardToken},{
stripeAccount: `${conn_id}`,
},
(err, card)=> {
console.log(err);
if(err){
return console.log("error attaching card "+ err)
}else{
const invoiceNo = admin.database().ref(`users/${userId}/something/somethingHistory/${subDbId}`)
return invoiceNo.once('value').then(snapshot=>{
const invoiceNumber = snapshot.child("invoiceId").val();
const subId = snapshot.child("subscriptionId").val();
return stripe.invoices.pay(
invoiceNumber,
{
expand: ['payment_intent','charge','subscription'],
},{
stripeAccount: `${conn_id}`,
},
(err, invoice)=>{
if(err){
return console.log("error paying invoice "+ err)
}else{
if(invoice.payment_intent.status==="succeeded"){
//DO SOME CODE
return console.log("New Payment succeeded for "+invoiceNumber)
}else{
//DO SOME OTHER CODE
//CANCEL
return stripe.subscriptions.del(
subId,{
stripeAccount: `${conn_id}`,
},
(err, confirmation)=> {
if(err){
return console.log("Subscription error")
}else{
return console.log("Subscription cancelled")
}
});
}
}
});
})
}
});
To me it looks like an incredibly inefficient / ugly way of achieving the effect and overall the user is sitting waiting for a response for approx 15 seconds although the function finishes its execution after 1862ms - I still get responses up to 5 - 10 seconds after.
What's the most efficient way of achieving the same desired effect of registering a new payment source, paying subscription and then handling the result of that payment?
You should use the Promises returned by the Stripe asynchronous methods, as follows (untested, it probably needs some fine tuning, in particular with the objects passed to the Stripe methods):
exports.onSecondPaymentAttempt = functions.database.ref("users/{userId}/something/somethingHistory/{subDbId}/newPayment").onCreate((snapshot, context) => {
var s = snapshot.val();
var fields = s.split(",");
const cardToken = fields[0];
const cus_id = fields[1];
const conn_id = fields[2];
const subDbId = context.params.subDbId;
const userId = context.params.userId;
return stripe.customers.createSource(
//Format of this object to be confirmed....
cus_id,
{ source: cardToken },
{ stripeAccount: `${conn_id}` }
)
.then(card => {
const invoiceNo = admin.database().ref(`users/${userId}/something/somethingHistory/${subDbId}`)
return invoiceNo.once('value')
})
.then(snapshot => {
const invoiceNumber = snapshot.child("invoiceId").val();
const subId = snapshot.child("subscriptionId").val();
return stripe.invoices.pay(
invoiceNumber,
{ expand: ['payment_intent', 'charge', 'subscription'] },
{ stripeAccount: `${conn_id}` }
)
})
.then(invoice => {
if (invoice.payment_intent.status === "succeeded") {
//DO SOME CODE
console.log("New Payment succeeded for " + invoiceNumber)
return null;
} else {
//DO SOME OTHER CODE
//CANCEL
return stripe.subscriptions.del(
subId, {
stripeAccount: `${conn_id}`,
});
}
})
.catch(err => {
//....
return null;
})
});
I would suggest you watch the three videos about "JavaScript Promises" from the official Firebase video series, which explain why it is key to correctly chain and return the promises returned by the asynchronous methods.

How to keep the loop running and send all the messages?

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

Categories