How to convert JavaScript functions that use multiple callbacks, to use promises? - javascript

I am trying to convert the way I use asynchronous functions from callbacks to promises.
I understand this basic conversion shown here, where callbacks are converted to the resolve and reject functions:
// CALLBACK
const getData = (id, callback) => {
setTimeout(() => {
if (!id) return callback('ERROR: id is missing')
return callback("The data for id " + id);
}, 1000)
};
getData(111, console.log);
getData(222, console.log);
getData(null, console.log);
// PROMISE
const getData2 = id => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!id) reject('id is missing');
resolve("The data for id " + id);
}, 1000);
});
};
getData2(333).then(console.log).catch((message) => console.log("ERROR: " + message));
getData2(444).then(console.log).catch((message) => console.log("ERROR: " + message));
getData2(null).then(console.log).catch((message) => console.log("ERROR: " + message));
But I often use callbacks as in the following scenerio where you can have a process which takes a long time and sends out bits of data in numerous callbacks back to the calling code as it processes its information:
sleep = function (ms) {
var start = new Date().getTime();
let now = 0;
let difference = 0;
for (var i = 0; i < 1e17; i++) {
now = new Date().getTime();
difference = now - start;
if (difference > ms) {
break;
}
}
}
const goShopping = (list, cbItemReport, cbFinished, cbError) => {
let count = 0;
let numberFound = 0;
const randomError = Math.floor(Math.random() * 3);
if (randomError == 0) {
cbError('Something went wrong, trip aborted.');
} else {
list.forEach(item => {
const randomFound = Math.floor(Math.random() * 4);
if (randomFound > 0) {
cbItemReport(item, true, ++count);
numberFound++;
} else {
cbItemReport(item, false, ++count);
}
sleep(1000);
})
cbFinished(`Bought ${numberFound} things.`);
}
}
goShopping(['milk', 'eggs', 'sugar', 'bread'],
(item, found, count) => {
console.log(`Item #${count} "${item}" was ${found ? 'found' : 'not found'}.`);
},
(message) => {
console.log("Returned from shopping: " + message);
},
(error) => {
console.log("ERROR: " + error);
});
How would I convert this latter use of callbacks to promises? In this case, the three callbacks cbItemReport, cbFinished, cbError are too many to map to the two that Promise has, i.e. only resolve (cbFinished) and reject (cbError), or what am I missing here?

From what it sounds like, you are looking to implement something like the RxJs library, so why not just use it?
Check out RxJs here
For example a call could look like this then:
const sub = new Subject(); // Create a subject
sub.asObservable().subscribe( // Subscribe to that subject as obserable
({item, found, count}) => { // next
console.log(`Item #${count} "${item}" was ${found ? 'found' : 'not found'}.`);
},
error => { // error
console.log("ERROR: " + error);
},
message => { // complete
console.log("Returned from shopping: " + message);
}
);
const goShopping = (list) => {
let count = 0;
let numberFound = 0;
const randomError = Math.floor(Math.random() * 3);
if (randomError == 0) {
sub.error('Something went wrong, trip aborted.'); // push an error to the subject
} else {
list.forEach(item => {
const randomFound = Math.floor(Math.random() * 4);
if (randomFound > 0) {
sub.next({item: item, found: true, count: ++count}); // push a result to subject (single object)
numberFound++;
} else {
sub.next({item: item, found: true, count: ++count}); // same as above
}
sleep(1000);
})
sub.complete(`Bought ${numberFound} things.`); // push complete to subject. after that no next is allowed anymore
}
}
goShopping(['milk', 'eggs', 'sugar', 'bread'])

Call resolve from cbFinished, reject from cbError and write a cbItemReport that does whatever you want it to do to each item, just as in the existing code.

Related

Loop with socket function doesn't work - WebSocket

I have a socket endpoint, that I connect to and send a message to get a user.
I used this code to do it :
import generateConnection from './generate-connection';
export async function fetchUser(id: number) {
return new Promise(function (resolve) {
const connection = generateConnection();
connection.onopen = () => {
connection.send(
JSON.stringify(
'{"msg":"connect","version":"1","support":["1","pre2","pre1"]}',
),
);
connection.send(
JSON.stringify(
`{"msg":"method","id":"1","method":"Users.getUser","params":[${id}]}`,
),
);
console.log('Connected');
};
connection.on('message', async (event) => {
const data = event.toString();
if (data[0] == 'a') {
const a = JSON.parse(JSON.parse(data.substring(1))[0]);
if (a.msg == 'result') {
if ('error' in a) {
console.log('Error' + a.error.msg);
return null;
} else {
resolve(a.result);
}
}
}
});
connection.on('error', function (error) {
console.log('Connection Error: ' + error.toString());
});
connection.on('close', function () {
console.log('echo-protocol Connection Closed');
});
});
}
const fetchAllUsers = async () => {
for (let i = 0; i < 100; i++) {
const user: any = await fetchUser(i);
console.log(user.name);
}
};
fetchAllUsers();
I get the following result :
Connected
Jack
It just give me the first user and it stop on the second.
I have no control over the socket and I want to be able to fetch all 5000 users each day to be synced.
I'm Using WebSocket for this problem.
If you have any proposition other than this method, I'm all ears :D
To explain more :
1 - I want to open a connection
2 - Send a message
3 - get Result
4 - Add to Array or db
5 - When finished, close the connection.
6 - repeat
Why would you close the connection every time? An open connection allows you to send many messages. But maybe it is easier to:
connection.close()
// right before:
resolve(a.result);
If that didn't work maybe it's time to send more then one request per connection. Try this (I'm a little rusty with promises so I hope you get the idea and improve it)
import generateConnection from './generate-connection';
export async function fetchAllUsers() {
var total = 100;
var returned = 0;
var all_results = [];
return new Promise(function(resolve) {
const connection = generateConnection();
connection.onopen = () => {
connection.send(
JSON.stringify(
'{"msg":"connect","version":"1","support":["1","pre2","pre1"]}',
),
);
for (var i = 0; i < total; i++) {
connection.send(
JSON.stringify(
`{"msg":"method","id":"1","method":"Users.getUser","params":[${i}]}`,
),
);
}
console.log('Connected');
};
connection.on('message', async(event) => {
const data = event.toString();
if (data[0] == 'a') {
const a = JSON.parse(JSON.parse(data.substring(1))[0]);
if (a.msg == 'result') {
if ('error' in a) {
console.log('Error' + a.error.msg);
return null;
} else {
console.log(a.result.name);
all_results.push(a.result);
returned++;
if (returned == total) {
resolve(all_results);
}
}
}
}
});
connection.on('error', function(error) {
console.log('Connection Error: ' + error.toString());
});
connection.on('close', function() {
console.log('echo-protocol Connection Closed');
});
});
}
fetchAllUsers();

Cloud Functions: Avoid Nesting Promises for different conditions

I am trying to write a function that takes into account 3 conditions whenever Stores/{storeId}/{departmentId}/{productId} gets triggered and write new data in ref.child('Home').child('Chiep').child(departmentId).child(productId).
1) When there is no data in firestore, I need to fill up all the fields in Realtime DB, by making queries in 2 different firestore's nodes: Stores and Products in order to take their images.
2) When a change is made in Stores node and it comes from the same {storeId}, I just need to update some data without making any additional query.
3) And finally, when a change is made in Stores node and it comes from other {storeId}, I need to make only one query in the Stores node.
exports.homeChiepest = functions.firestore
.document('Stores/{storeId}/{departmentId}/{productId}')
.onWrite((change, context) => {
const storeId = context.params.storeId;
const departmentId = context.params.departmentId;
const productId = context.params.productId;
const ref = admin.database().ref();
// Get an object with the current document value.
// If the document does not exist, it has been deleted.
const document = change.after.exists ? change.after.data() : null;
// Get an object with the previous document value (for update or delete)
const oldDocument = change.before.exists ? change.before.data() : null;
// Prevent infinite loops
if (!change.after.exists) {
console.log('DATA DELETED RETURN NULL');
return null;
}
const newPrice = document.price;
const newTimestamp = document.timestamp;
return ref.child('Home').child('Chiep')
.child(departmentId).child(productId)
.once('value')
.then(dataSnapshot => {
if (dataSnapshot.val() !== null) {
console.log('CHIEP DOES exist');
const oldPrice = dataSnapshot.val().price;
const storeKey = dataSnapshot.val().storeKey;
if (storeId === storeKey) {
console.log('SAME STORE - Change price and timestamp');
var newChiepest = {
timestamp: newTimestamp,
price: newPrice
};
return dataSnapshot.ref.update(newChiepest);
} else {
console.log('OTHER STORE - Verify if price is chieper...');
if (newPrice <= oldPrice) {
console.log('NEW PRICE: '+newPrice+' is chieper than the older one: '+oldPrice);
return change.after.ref.parent.parent.get().then(doc => { // HERE Avoid nesting promises
newStoreImg = doc.data().image;
var newStoreChiep = {
price: newPrice,
storeImg: newStoreImg,
storeKey: storeId,
timestamp: newTimestamp
};
return dataSnapshot.ref.update(newStoreChiep);
});
} else {
console.log('NEW PRICE: '+newPrice+' is mode EXPENSIVE than the older one: '+oldPrice);
}
return null;
}
} else {
console.log('data does NOT exist, so WRITE IT!');
let getStoreData = change.after.ref.parent.parent.get();
let getProductData = admin.firestore().collection('Products').doc('Departments').collection(departmentId).doc(productId).get();
return Promise.all([getStoreData, getProductData]).then(values => { // HERE Avoid nesting promises
const [store, product] = values;
var newHomeChiepest = {
depId: departmentId,
price: newPrice,
prodImg: product.data().image,
prodKey: productId,
storeKey: storeId,
storeImg: store.data().image,
timestamp: newTimestamp
};
return dataSnapshot.ref.set(newHomeChiepest);
});
}
})
.catch(error => {
console.log('Catch error reading Home: ',departmentId ,'/', productId,'; message: ',error);
return false;
});
});
The problem is: different possibilities of querying or not querying another firestore node led me to a warning while uploading the Clound Function, that is:
warning Avoid nesting promises promise/no-nesting
I appreciate any help to refactor this code.
You could use a variable to manage a "shunting", depending on the different cases, as follows (untested):
exports.homeChiepest = functions.firestore
.document('Stores/{storeId}/{departmentId}/{productId}')
.onWrite((change, context) => {
const storeId = context.params.storeId;
const departmentId = context.params.departmentId;
const productId = context.params.productId;
const ref = admin.database().ref();
const document = change.after.exists ? change.after.data() : null;
// Prevent infinite loops
if (!change.after.exists) {
console.log('DATA DELETED RETURN NULL');
return null;
}
const newPrice = document.price;
const newTimestamp = document.timestamp;
let shunting; // <-- We manage the shunting through this variable
let chiepRef;
return ref.child('Home').child('Chiep')
.child(departmentId).child(productId)
.once('value')
.then(dataSnapshot => {
chiepRef = dataSnapshot.ref;
if (dataSnapshot.val() !== null) {
console.log('CHIEP DOES exist');
const oldPrice = dataSnapshot.val().price;
const storeKey = dataSnapshot.val().storeKey;
if (storeId === storeKey) {
shunting = 1
console.log('SAME STORE - Change price and timestamp');
var newChiepest = {
timestamp: newTimestamp,
price: newPrice
};
return chiepRef.update(newChiepest);
} else {
console.log('OTHER STORE - Verify if price is chieper...');
if (newPrice <= oldPrice) {
console.log('NEW PRICE: ' + newPrice + ' is chieper than the older one: ' + oldPrice);
shunting = 2
return change.after.ref.parent.parent.get();
} else {
console.log('NEW PRICE: ' + newPrice + ' is mode EXPENSIVE than the older one: ' + oldPrice);
shunting = 3
return null;
}
}
} else {
console.log('data does NOT exist, so WRITE IT!');
shunting = 4;
let getStoreData = change.after.ref.parent.parent.get();
let getProductData = admin.firestore().collection('Products').doc('Departments').collection(departmentId).doc(productId).get();
return Promise.all([getStoreData, getProductData])
}
})
.then(result => {
if (shunting === 2) {
const newStoreImg = result.data().image;
var newStoreChiep = {
price: newPrice,
storeImg: newStoreImg,
storeKey: storeId,
timestamp: newTimestamp
};
return chiepRef.update(newStoreChiep);
} else if (shunting === 4) {
const [store, product] = result;
const newHomeChiepest = {
depId: departmentId,
price: newPrice,
prodImg: product.data().image,
prodKey: productId,
storeKey: storeId,
storeImg: store.data().image,
timestamp: newTimestamp
};
return chiepRef.set(newHomeChiepest);
} else {
return null;
}
})
.catch(error => {
console.log('may be adapted, function of shunting', error);
return null;
});
});

Executing functions in order

Basically, I'm making a small script to check the IP of some servers based on their hostname, then compare that IP to a list based on the IP block a router is issuing.
The problem I'm having is clearly with the code executing asynchronously, and it's likely been answered a thousand times, but I can't seem to wrap my head around how to fix it. I've tried wrapping everything in promises but I end up breaking everything. Here is the latest attempt to break the steps I need out into individual functions.
const dns = require('dns');
const Table = require('cli-table');
const hosts = ['Server01', 'Server02', 'Server03'];
let list = [];
table = new Table({
head: ['Host', 'Location']
, colWidths: [20, 30]
});
function process() {
hosts.forEach(host => {
dns.lookup(host, function (err, result) {
ipSplit = result.split(".");
r = ipSplit[0] + '.' + ipSplit[1] + '.' + ipSplit[2];
if (r == '10.23.13') {
list.push([host, 'Lab A112']);
}
else {
list.push([host, 'Unknown']);
}
});
});
};
function build () {
table.push(list);
};
function push () {
console.log(table.toString());
};
process();
build();
push();
What piece of the puzzle am I missing here?
You'll want to use Promise.all:
const result = Promise.all(hosts.map(host => {
return new Promise((resolve, reject) => {
dns.lookup(host, function (err, result) {
if (err) reject(err);
const ipSplit = result.split(".");
const r = ipSplit[0] + '.' + ipSplit[1] + '.' + ipSplit[2];
if (r === '10.23.13') {
resolve([host, 'Lab A112']);
} else {
resolve([host, 'Unknown']);
}
});
}
}));
You can order your function calls with async/await and you will get the order you need.
const dns = require('dns');
const Table = require('cli-table');
const hosts = ['Server01', 'Server02', 'Server03'];
let list = [];
table = new Table({
head: ['Host', 'Location']
, colWidths: [20, 30]
});
function process() {
return new Promise((resolve, reject) => {
hosts.forEach(host => {
dns.lookup(host, function (err, result) {
ipSplit = result.split(".");
r = ipSplit[0] + '.' + ipSplit[1] + '.' + ipSplit[2];
if (r == '10.23.13') {
resolve(list.push([host, 'Lab A112']));
}
else {
reject(list.push([host, 'Unknown']));
}
});
});
})
};
function build () {
return new Promise((resolve, reject)=>{
resolve(table.push(list);)
})
};
function push () {
console.log(table.toString());
};
async function waitForFunctions() {
try{
const resOne = await process();
const resTwo = await build()
} catch(error){
console.log(error);
}
return Promise.all([resOne, resTwo])
}
waitForFunctions()
.then((values)=>{
console.log(values);
console.log(push());
});

How to prevent a race condition in javascript?

I am working on a Resource Manager and some jobs must be atomic, because there may be race condition.
I see a solution is provided in a similar post
Javascript semaphore / test-and-set / lock?
which says implement a basic integer semaphore, just add the variable into the DOM and lock/unlock it and make sure your functions keep checking it, else timeout them
Can someone help me how can I implement it for below code ? and How can I add a variable to DOM ?
In the code below retrievePartners methods returns partners based on what user asked in capabilities object, and retrievePartners method may have been called same time from another user, asking for same partners (browsers.) So this method should be atomic, means should deal only with one capabilities at a same time.
async retrievePartners (capabilities) {
const appropriatePartners = { chrome: [], firefox: [], safari: [], ie: [] }
const partners = await this.getAllPartners()
allTypeNumber = 0
// first check if there is available appropriate Partners
Object.keys(capabilities.type).forEach(key => {
let typeNumber = parseInt(capabilities.type[key])
allTypeNumber = allTypeNumber + typeNumber
for (let i = 0; i < typeNumber; i++) {
partners.forEach((partner, i) => {
if (
key === partner.value.type &&
partner.value.isAvailable &&
appropriatePartners[key].length < typeNumber
) {
appropriatePartners[key].push(partner)
}
})
if (appropriatePartners[key].length < typeNumber) {
throw new Error(
'Sorry there are no appropriate Partners for this session'
)
}
}
})
if (allTypeNumber === 0) {
throw new Error('Please mention at least 1 type of browser !')
} else {
Object.keys(appropriatePartners).forEach(key => {
appropriatePartners[key].forEach(partner => {
this.instructorPeer.set('/partners/' + partner.id + '/states/', {
isAvailable: false
})
})
})
return appropriatePartners
}
}
getAllPartners method
async getAllPartners (capabilities) {
const partners = []
const paths = await this.instructorPeer.get({
path: { startsWith: '/partners/' }
})
paths.forEach((path, i) => {
if (path.fetchOnly) {
let obj = {}
obj.value = path.value
obj.id = path.path.split('/partners/')[1]
obj.value.isAvailable = paths[i + 1].value.isAvailable
partners.push(obj)
}
})
return partners
}
Here is how I call the method
async function startTest () {
const capabilities = {
type: {
chrome: 1
}
}
test('Example test', async t => {
try {
session = await workout.createSession(capabilities)
const partners = session.partners
const partner = partners.chrome[0]
...

React native and Firebase promises - weird side effect on render

I'm using two chained firebase requests in my code. the first one is fetching all the reviews, then I iterate trough all the results, firing a second firebase request for each element in loop.
After the loop, I've updated the state with the new data using setState. And that update is making my GUI transparent, like so:
the bottom part is randomly transparent each time, sometimes the render is visible a bit, sometimes not at all. When I remove the setState block, everything is fine.
The code in question:
getReviews() {
let reviewData = {}
let vendorId = this.props.item.vendor_id
let product = this.props.item.key
firebase.database().ref('u_products/' + vendorId + '/' + product + '/reviews/').once('value', (data) => {
reviewData = data.val()
if (!reviewData) {
this.setState({
loadedReviews: true
})
return
} else {
for (var key in reviewData) {
firebase.database().ref('users/' + reviewData[key].poster_uid + '/').once('value', (data) => {
let userData = data.val()
if (userData) {
this.getUserImageUrl()
...
}
})
}
this.state.reviewData = reviewData
this.setState({
dataSource: this.state.dataSource.cloneWithRows(reviewData),
loadedReviews: true,
})
}
})
}
Intended behaviour is first firebase request -> second one iterating for all the results from the first request ->setState.
Does anyone else have this issue?
firebase's .once() returns a Promise, so what you need to do is create an array of Promises, then call Promise.all(array).then((arrayOfData) => { ... });
You can now process the resulting array, then call .setState()
Here's a mockup:
/* firebase mockup */
function Result(url) {
this.url = url;
this.once = function(param, callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
var data = {
val: () => {
return this.url + " query result";
}
};
resolve(data);
}, Math.random() * 500 + 1000);
});
};
}
var firebase = {
database: function() {
return firebase;
},
ref: function(url) {
return new Result(url);
}
};
var reviewData = {
"test": {
poster_uid: "testData"
},
"2nd": {
poster_uid: "2ndData"
}
};
// ACTUAL IMPORTANT CODE BELOW #################
var arrayOfPromises = [];
for (var key in reviewData) {
arrayOfPromises.push(firebase.database().ref('users/' + reviewData[key].poster_uid + '/').once('value'));
}
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
arrayOfResults.map((data, i) => {
let userData = data.val();
if (userData) {
console.log(i, userData);
}
});
console.log("setting state now");
});

Categories