Wait for all Firebase data query requests before executing code - javascript

I am trying to fetch data from different collections in my cloud Firestore database in advance before I process them and apply them to batch, I created two async functions, one to capture the data and another to execute certain code only after all data is collected, I didn't want the code executing and creating errors before the data is fetched when i try to access the matchesObject after the async function to collect data is finished, it keeps saying "it cannot access a property matchStatus of undefined", i thought took care of that with async and await? could anyone shed some light as to why it is undefined one moment
axios.request(options).then(function(response) {
console.log('Total matches count :' + response.data.matches.length);
const data = response.data;
var matchesSnapshot;
var marketsSnapshot;
var tradesSnapshot;
var betsSnapshot;
matchesObject = {};
marketsObject = {};
tradesObject = {};
betsObject = {};
start();
async function checkDatabase() {
matchesSnapshot = await db.collection('matches').get();
matchesSnapshot.forEach(doc => {
matchesObject[doc.id] = doc.data();
console.log('matches object: ' + doc.id.toString())
});
marketsSnapshot = await db.collection('markets').get();
marketsSnapshot.forEach(doc2 => {
marketsObject[doc2.id] = doc2.data();
console.log('markets object: ' + doc2.id.toString())
});
tradesSnapshot = await db.collection('trades').get();
tradesSnapshot.forEach(doc3 => {
tradesObject[doc3.id] = doc3.data();
console.log('trades object: ' + doc3.id.toString())
});
betsSnapshot = await db.collection('bets').get();
betsSnapshot.forEach(doc4 => {
betsObject[doc4.id] = doc4.data();
console.log('bets object: ' + doc4.id.toString())
});
}
async function start() {
await checkDatabase();
// this is the part which is undefined, it keeps saying it cant access property matchStatus of undefined
console.log('here is matches object ' + matchesObject['302283']['matchStatus']);
if (Object.keys(matchesObject).length != 0) {
for (let bets of Object.keys(betsObject)) {
if (matchesObject[betsObject[bets]['tradeMatchId']]['matchStatus'] == 'IN_PLAY' && betsObject[bets]['matched'] == false) {
var sfRef = db.collection('users').doc(betsObject[bets]['user']);
batch11.set(sfRef, {
accountBalance: admin.firestore.FieldValue + parseFloat(betsObject[bets]['stake']),
}, {
merge: true
});
var sfRef = db.collection('bets').doc(bets);
batch12.set(sfRef, {
tradeCancelled: true,
}, {
merge: true
});
}
}
}
});

There are too many smaller issues in the current code to try to debug them one-by-one, so this refactor introduces various tests against your data. It currently won't make any changes to your database and is meant to be a replacement for your start() function.
One of the main differences against your current code is that it doesn't unnecessarily download 4 collections worth of documents (two of them aren't even used in the code you've included).
Steps
First, it will get all the bet documents that have matched == false. From these documents, it will check if they have any syntax errors and report them to the console. For each valid bet document, the ID of it's linked match document will be grabbed so we can then fetch all the match documents we actually need. Then we queue up the changes to the user's balance and the bet's document. Finally we report about any changes to be done and commit them (once you uncomment the line).
Code
Note: fetchDocumentById() is defined in this gist. Its a helper function to allow someCollectionRef.where(FieldPath.documentId(), 'in', arrayOfIds) to take more than 10 IDs at once.
async function applyBalanceChanges() {
const betsCollectionRef = db.collection('bets');
const matchesCollectionRef = db.collection('matches');
const usersCollectionRef = db.collection('users');
const betDataMap = {}; // Record<string, BetData>
await betsCollectionRef
.where('matched', '==', false)
.get()
.then((betsSnapshot) => {
betsSnapshot.forEach(betDoc => {
betDataMap[betDoc.id] = betDoc.data();
});
});
const matchDataMap = {}; // Record<string, MatchData | undefined>
// betIdList contains all IDs that will be processed
const betIdList = Object.keys(betDataMap).filter(betId => {
const betData = betDataMap[betId];
if (!betData) {
console.log(`WARN: Skipped Bet #${betId} because it was falsy (actual value: ${betData})`);
return false;
}
const matchId = betData.tradeMatchId;
if (!matchId) {
console.log(`WARN: Skipped Bet #${betId} because it had a falsy match ID (actual value: ${matchId})`);
return false;
}
if (!betData.user) {
console.log(`WARN: Skipped Bet #${betId} because it had a falsy user ID (actual value: ${userId})`);
return false;
}
const stakeAsNumber = Number(betData.stake); // not using parseFloat as it's too lax
if (isNaN(stakeAsNumber)) {
console.log(`WARN: Skipped Bet #${betId} because it had an invalid stake value (original NaN value: ${betData.stake})`);
return false;
}
matchDataMap[matchId] = undefined; // using undefined because its the result of `doc.data()` when the document doesn't exist
return true;
});
await fetchDocumentsById(
matchesCollectionRef,
Object.keys(matchIdMap),
(matchDoc) => matchDataMap[matchDoc.id] = matchDoc.data()
);
const batch = db.batch();
const queuedUpdates = 0;
betIdList.forEach(betId => {
const betData = betDataMap[betId];
const matchData = matchDataMap[betData.tradeMatchId];
if (matchData === undefined) {
console.log(`WARN: Skipped /bets/${betId}, because it's linked match doesn't exist!`);
continue;
}
if (matchData.matchStatus !== 'IN_PLAY') {
console.log(`INFO: Skipped /bets/${betId}, because it's linked match status is not "IN_PLAY" (actual value: ${matchData.matchStatus})`);
continue;
}
const betRef = betsCollectionRef.doc(betId);
const betUserRef = usersCollectionRef.doc(betData.user);
batch.update(betUserRef, { accountBalance: admin.firestore.FieldValue.increment(Number(betData.stake)) });
batch.update(betRef, { tradeCancelled: true });
queuedUpdates += 2; // for logging
});
console.log(`INFO: Batch currently has ${queuedUpdates} queued`);
// only uncomment when you are ready to make changes
// batch.commit();
}
Usage:
axios.request(options)
.then(function(response) {
const data = response.data;
console.log('INFO: Total matches count from API:' + data.matches.length);
return applyBalanceChanges();
}

Related

JS - How to retrieve variable after IndexedDB transaction.oncomplete() executes?

My problem is simple, but incredibly frustrating as I'm now on my second week of trying to figure this out and on the verge of giving up. I would like to retrieve my 'notesObject' variable outside my getAllNotes() function when after the transaction.oncomplete() listener executes.
(function() {
// check for IndexedDB support
if (!window.indexedDB) {
console.log(`Your browser doesn't support IndexedDB`);
return;
}
// open the CRM database with the version 1
let request = indexedDB.open('Notes', 1);
// create the Contacts object store and indexes
request.onupgradeneeded = (event) => {
let db = event.target.result;
// create the Notes object store ('table')
let store = db.createObjectStore('Notes', {
autoIncrement: true
});
// create an index on the sections property.
let index = store.createIndex('Sections', 'sections', {
unique: true
});
}
function insertData() {
let myDB = indexedDB.open('Notes');
myDB.onsuccess = (event) => {
// myDB.transaction('Notes', 'readwrite')
event.target.result.transaction('Notes', 'readwrite')
.objectStore('Notes')
.put({
sections: "New Note",
pages: "New page",
lastSelectedPage: ""
});
console.log("insert successful");
}
myDB.onerror = (event) => {
console.log('Error in NotesDB - insertData(): ' + event.target.errorCode);
}
myDB.oncomplete = (event) => {
myDB.close();
console.log('closed');
}
}
insertData()
function getAllNotes() {
let myDB = indexedDB.open('Notes');
let notesObject = [];
myDB.onsuccess = (event) => {
let dbObjectStore = event.target.result
.transaction("Notes", "readwrite").objectStore("Notes");
dbObjectStore.openCursor().onsuccess = (e) => {
let cursor = e.target.result;
if (cursor) {
let primaryKey = cursor.key;
let section = cursor.value.sections;
notesObject.push({
primaryKey,
section
})
cursor.continue();
}
}
dbObjectStore.transaction.onerror = (event) => {
console.log('Error in NotesDB - getAllData() tranaction: ' + event.target.errorCode);
}
dbObjectStore.transaction.oncomplete = (event) => {
return notesObject;
console.log(notesObject)
}
}
}
let notes = getAllNotes()
console.log("Getting Notes sucessful: " + notes)
})()
I've tried setting global variables, but nothing seems to work. I am a complete noob and honestly, I'm completely lost on how to retrieve the notesObject variable outside my getAllNotes() function. The results I get are 'undefined'. Any help would be greatly appreciated.
This is effectively a duplicate of Indexeddb: return value after openrequest.onsuccess
The operations getAllNotes() kicks off are asynchronous (they will run in the background and take time to complete), whereas your final console.log() call is run synchronously, immediately after getAllNotes(). The operations haven't completed at the time that is run, so there's nothing to log.
If you search SO for "indexeddb asynchronous" you'll find plenty of questions and answers about this topic.

Updating data in an Object using Firebase arrayUnion

I am looking to update data in an object without changing the index of the object within the array it is contained. As it currently stands, the code removes the current object from the array and then applies array Union to update the array but pushes the component to the end of the array but I am looking to just update the data without the component losing its index. This is the code I am currently working with, I looked through the Firebase docs to see if there was a way to just update the component but couldn't find anything if anyone could point me in the right direction, please.
await firestore.update(project, {
pages: firestore.FieldValue.arrayRemove(page),
});
await firestore.update(project, {
pages: firestore.FieldValue.arrayUnion(newPage),
});
Unfortunately there is no field transform to replace a value like this:
firestore.FieldValue.arrayReplace(page, newPage);
Storing arrays and making changes by index in remote databases is generally discouraged. This older Firebase blog post covers some of the reasons why even though it was written with the Firebase Realtime Database in mind.
If the order of that array is important, you have two options:
fetch the array, mutate it, and then write it back. (simple)
fetch the array, find the relevant index, update that index only. (difficult)
To achieve the first result, you would make use of a transaction to find the previous value and replace it:
const db = firebase.firestore();
const projectDocRef = db.doc("projects/projectId");
function replacePage(oldPage, newPage) {
return db.runTransaction(async (t) => {
const snapshot = await t.get(projectDocRef);
if (!snapshot.exists) {
// no previous data, abort.
return "aborted";
}
const pagesArray = snapshot.get("pages");
const index = pagesArray.findIndex((page) => page === oldPage);
if (index === -1)
return "not-found";
pagesArray[index] = newPage;
await t.set(projectDocRef, { pages: pagesArray }, { merge: true });
return "replaced";
});
}
replacePage("index", "shop")
.then((result) => console.log("Page replacement was " + (result === "replaced" ? "" : " not") + " successful"))
.catch((err) => console.error('failed: ', err));
Note: Anything beyond this point is educational. There are many issues with this approach at scale.
Because Firestore doesn't support array entry replacement by index, you'll need to implement a way to update an index using something Firestore understands - maps. Using some FirestoreDataConverter trickery, you can use the converter to serialize your array as a map when you write it to Cloud Firestore and deserialize it back to an array when you read it. The major trade-off here is in how you will be able to query your data. You will be able to perform queries by index (such as where('pages.0', '==', 'shop')) but you'll lose the ability to use array-contains queries (such as where('pages', 'array-contains', 'shop')).
First, you need to define the converter:
// const obj = {};
// setNestedProperty(obj, ["a", "b", "c"], true)
// obj is now { "a": { "b": { "c": true } } }
function setNestedProperty(originalObj, pathPropsArray, val) {
const props = pathPropsArray.slice(0, -1);
const lastProp = pathPropsArray[pathPropsArray.length-1];
const parent = props.reduce((obj, p) => obj[p] ? obj[p] : (obj[p] = {}), originalObj);
parent[lastProp] = val;
}
const pagesArrayConverter = {
toFirestore(data) {
if (data.pages !== undefined) {
// step 1) convert array to map
const pagesAsMap = {};
data.pages.forEach((page, index) => {
if (page !== undefined) {
pagesAsMap[index] = page;
}
});
data.pages = pagesAsMap;
// step 2) if there are any mutations to "pages"
// while you are changing it, make the
// changes now before uploading to Firestore
Object.keys(data)
.filter(k => k.startsWith("pages."))
.forEach(k => {
const nestedValue = data[k];
data[k] = undefined;
delete data[k];
setNestedProperty(pagesAsMap, k.slice(6).split("."), nestedValue);
});
}
return data;
},
fromFirestore(snapshot, options) {
const data = snapshot.data(options);
if (data.pages !== undefined) {
const pagesAsArray = [];
Object.entries(data.pages)
.map(([index, page]) => pagesAsArray[index] = page);
// `pagesAsArray` may have empty elements, so we need
// to fill in the gaps with `undefined`:
data.pages = Array.from(pagesAsArray);
}
return data;
}
};
Which you would then attach to a query/reference like this:
const db = firebase.firestore();
const projectDocRef = db.doc("projects/projectId")
.withConverter(pagesArrayConverter)
If you already know that the previous value has an index of 2, you can just use:
await projectDocRef.set({ "pages.2": newPage }, { merge: true });
If you need to find it like before, you can use a transaction:
function replacePage(oldPage, newPage) {
return db.runTransaction(aysnc (t) => {
const snapshot = await t.get(projectDocRef);
if (!snapshot.exists) {
// no previous data, abort.
return "missing";
}
const data = snapshot.data();
// data is a { pages: Page[] }
const index = data.pages.findIndex((page) => page === oldPage);
if (index === -1)
return "not-found";
await t.set(projectDocRef, { ["pages." + oldIndex]: newPage }, { merge: true });
return "replaced";
});
}
replacePage("index", "shop")
.then((result) => console.log("Page replacement was " + (result === "replaced" ? "" : " not") + " successful"))
.catch((err) => console.error('failed: ', err));
arrayUnion adds new items to the array and arrayRemove removes items from an array. There isn't any way to update an existing item in array directly.
You would have to fetch the document, manually add/update the item at relevant index and then update the whole array back to the document.

React-Native — Global variable not changing

I have two separate files, the first being a component (List.js) that uses the second (APIService.js) to fetch different APIs. To correct fetch, the URL needs to receive global variables. Right now, I am trying to redefine these variables from a function in the APIService file without success. Variables are being redefined in APIService.js just before the API calls comment.
I have two questions:
Why is the global variable naptanId not being redefined?
Would be possible to define and pass these variables from the component?
Pseudo-code
Detects beacon
Redefine naptanId
Component fetch API using recently defined variable
API call is done
Data is passed back to Component
Set states
List.js
componentDidMount() {
// Executes first function
APIService._fetchStopPoint((resp1) => {
console.log("Stoppoint", resp1)
// ... and set the bus state with the first response
this.setState({
bus: resp1
});
// ... based on the response, convert array to string
const lines = (resp1.lines.map((line) => line.name)).toString()
// ... pass lines to sencond function
APIService._fetchArrivalTimes(lines, (resp2) => {
// .. and set the tube state with the second response
this.setState({
isLoading: false,
tube: resp2
});
});
});
}
APIService.js
// Variables
// ***********************************************************************
let naptanId = undefined
let lines = undefined
let ice = '59333'
let mint = '57011'
let blueberry = '27686'
let nearestBeacon = undefined;
let newBeaconId = undefined;
let setIce = false;
let setBlueberry = false;
let setMint = false;
// Beacon detection
// ***********************************************************************
const region = {
identifier: 'Estimotes',
uuid: '354A97D8-9CAF-0DC7-CE0E-02352EBE90CD',
};
// Request for authorization while the app is open
Beacons.requestWhenInUseAuthorization();
Beacons.startMonitoringForRegion(region);
Beacons.startRangingBeaconsInRegion(region);
Beacons.startUpdatingLocation();
// Listen for beacon changes
const subscription = DeviceEventEmitter.addListener('beaconsDidRange', (data) => {
const ibeacons = data.beacons
// var lowestAccuracySeen = 0.5;
let lowestAccuracySeen = "immediate"
// Check if beacons are updating
if (ibeacons && ibeacons.length > 0) {
// Loop through beacons array
for (var i = 0; i < ibeacons.length ; i++) {
// Find beacons with same minor ...
var foundBeacon = ibeacons.find(function(closestBeacon) {
// ... and return the beacon the lowest accuracy seen
// return closestBeacon.accuracy.toFixed(2) < lowestAccuracySeen;
return closestBeacon.proximity == lowestAccuracySeen
});
// If found ...
if (foundBeacon) {
// ... define the lowest accuracy and the nearest beacon
lowestAccuracySeen = foundBeacon.accuracy;
nearestBeacon = foundBeacon;
// Identify what component to render against nearest beacon
setIce = nearestBeacon.minor == ice ? true : false;
setMint = nearestBeacon.minor == mint ? true : false;
setBlueberry = nearestBeacon.minor == blueberry ? true : false;
if (setIce) {
// THESE VARIABLES CANNOT BE REDEFINED
naptanId = "490004936E"
lines = "55"
} else if (setMint) {
} else if (setBlueberry) {
};
}
}
}
});
// API calls
// ***********************************************************************
class APIService {
// Fecth stop point info
static _fetchStopPoint(cb) {
console.log(naptanId, lines)
fetch(`https://api.tfl.gov.uk/StopPoint/${naptanId}`)
.then(stopData => {
try {
stopData = JSON.parse(stopData._bodyText); // Converts data to a readable format
cb(stopData, naptanId);
} catch(e) {
cb(e);
}
})
.catch(e => cb(e));
}
// Fetch arrival times info
static _fetchArrivalTimes(lines, cb) {
fetch(`https://api.tfl.gov.uk/Line/${lines}/Arrivals/${naptanId}`)
.then(arrivalData => {
try {
arrivalData = JSON.parse(arrivalData._bodyText);
arrivalTime = arrivalData
cb(arrivalData);
} catch(e) {
cb(e);
}
})
.catch(e => cb(e));
}
// Fetch status info
static _fetchStatus(lines) {
fetch(`https://api-argon.digital.tfl.gov.uk/Line/${lines}/Status`)
.then(statusData => {
try {
statusData = JSON.parse(statusData._bodyText); // Converts data to a readable format
cb(statusData);
} catch(e) {
cb(e);
}
})
.catch(e => cb(e));
}
}
module.exports = APIService;
The simplest approach to handle these global variables (cross different components) is to use AsyncStorage:
let response = await AsyncStorage.getItem('listOfTasks'); //get, in any components
AsyncStorage.setItem('listOfTasks', 'I like to save it.'); //set, in any components
For more performance critical global vars, you can also consider Realm Database (Like CoreData, SQLite in both iOS and Android).

Firebase: Run a query synchronously

I am trying to set some user data depending on the no.of users already in my USERS COLLECTION. This even includes a userId which should be a number.
exports.setUserData = functions.firestore.document('/users/{documentId}')
.onCreate(event => {
return admin.firestore().collection('users')
.orderBy('userId', 'desc').limit(1)
.get().then(function(snapshot) {
const user = snapshot.docs[0].data();
var lastUserId = user.userId;
var userObject = {
userId: lastUserId + 1,... some other fields here
};
event.data.ref.set(userObject, {
merge: true
});
});
});
One issue I noticed here, quickly adding 2 users result in those documents having the same userId may be because the get() query is asynchronous?
Is there a way to make this whole setUserData method synchronous?
There is no way to make Cloud Functions run your function invocations sequentially. That would also be quite contrary to the serverless promise of auto-scaling to demands.
But in your case there's a much simpler, lower level primitive to get a sequential ID. You should store the last known user ID in the database and then use a transaction to read/update it.
var counterRef = admin.firestore().collection('counters').doc('userid');
return db.runTransaction(function(transaction) {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(counterRef).then(function(counterDoc) {
var newValue = (counterDoc.data() || 0) + 1;
transaction.update(counterRef, newValue);
});
});
Solution
var counterRef = admin.firestore().collection('counters').doc('userId');
return admin.firestore().runTransaction(function(transaction) {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(counterRef).then(function(counterDoc) {
var newValue = (counterDoc.data().value || 0) + 1;
transaction.update(counterRef, {
"value": newValue
});
});
}).then(t => {
admin.firestore().runTransaction(function(transaction) {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(counterRef).then(function(counterDoc) {
var userIdCounter = counterDoc.data().value || 0;
var userObject = {
userId: userIdCounter
};
event.data.ref.set(userObject, {
merge: true
});
});
})
});

Limit number of records in firebase

Every minute I have a script that push a new record in my firebase database.
What i want is delete the last records when length of the list reach a fixed value.
I have been through the doc and other post and the thing I have found so far is something like that :
// Max number of lines of the chat history.
const MAX_ARDUINO = 10;
exports.arduinoResponseLength = functions.database.ref('/arduinoResponse/{res}').onWrite(event => {
const parentRef = event.data.ref.parent;
return parentRef.once('value').then(snapshot => {
if (snapshot.numChildren() >= MAX_ARDUINO) {
let childCount = 0;
let updates = {};
snapshot.forEach(function(child) {
if (++childCount <= snapshot.numChildren() - MAX_ARDUINO) {
updates[child.key] = null;
}
});
// Update the parent. This effectively removes the extra children.
return parentRef.update(updates);
}
});
});
The problem is : onWrite seems to download all the related data every time it is triggered.
This is a pretty good process when the list is not so long. But I have like 4000 records, and every month it seems that I screw up my firebase download quota with that.
Does anyone would know how to handle this kind of situation ?
Ok so at the end I came with 3 functions. One update the number of arduino records, one totally recount it if the counter is missing. The last one use the counter to make a query using the limitToFirst filter so it retrieve only the relevant data to remove.
It is actually a combination of those two example provided by Firebase :
https://github.com/firebase/functions-samples/tree/master/limit-children
https://github.com/firebase/functions-samples/tree/master/child-count
Here is my final result
const MAX_ARDUINO = 1500;
exports.deleteOldArduino = functions.database.ref('/arduinoResponse/{resId}/timestamp').onWrite(event => {
const collectionRef = event.data.ref.parent.parent;
const countRef = collectionRef.parent.child('arduinoResCount');
return countRef.once('value').then(snapCount => {
return collectionRef.limitToFirst(snapCount.val() - MAX_ARDUINO).transaction(snapshot => {
snapshot = null;
return snapshot;
})
});
});
exports.trackArduinoLength = functions.database.ref('/arduinoResponse/{resId}/timestamp').onWrite(event => {
const collectionRef = event.data.ref.parent.parent;
const countRef = collectionRef.parent.child('arduinoResCount');
// Return the promise from countRef.transaction() so our function
// waits for this async event to complete before it exits.
return countRef.transaction(current => {
if (event.data.exists() && !event.data.previous.exists()) {
return (current || 0) + 1;
} else if (!event.data.exists() && event.data.previous.exists()) {
return (current || 0) - 1;
}
}).then(() => {
console.log('Counter updated.');
});
});
exports.recountArduino = functions.database.ref('/arduinoResCount').onWrite(event => {
if (!event.data.exists()) {
const counterRef = event.data.ref;
const collectionRef = counterRef.parent.child('arduinoResponse');
// Return the promise from counterRef.set() so our function
// waits for this async event to complete before it exits.
return collectionRef.once('value')
.then(arduinoRes => counterRef.set(arduinoRes.numChildren()));
}
});
I have not tested it yet but soon I will post my result !
I also heard that one day Firebase will add a "size" query, that is definitely missing in my opinion.

Categories

Resources