A Cloud Function is set up to trigger when a new entry is created, and it checks whether we have enough players to start a game. If so, it creates the game at /matches , and writes the gameId to each player's entry, like so: /users/{playerId}/profile/cm
Players listen to gameId on their own database entries, so they know when to transition to the GamePlay screen.
Since there are a lot of players request for find opponent simultaneously, and the Cloud Function is asynchronous concurrency , the concurrency issues come out and as you can see in the picture one user added in two diffect matches.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.onNewRequest = functions.database.ref("/requests/{boardId}/{userId}").onCreate((change, context) => {
var boardId = context.params.boardId;
var createdUserId = context.params.boardId;
// new request created
var requests = admin.database().ref("/requests/"+boardId);
requests.once("value").then((requestsSnapshot)=>{
var waitersCount = requestsSnapshot.numChildren();
if(waitersCount > 1){
// more than one user waiting to join the game
// lets link users
var creatorSnapshot;
var guestSnapshot;
// more than two players waiting to play a game on the board{boardId}
var index = 0;
requestsSnapshot.forEach((childSnapshot) => {
if (index === 0) creatorSnapshot = childSnapshot;
if (index === 1) guestSnapshot = childSnapshot;
index++;
});
// we have the creator and the guest
// creatorSnapshot.key is the id of the user /requests/boardId/userId <<<<--
var match = admin.database().ref("/matches").push({
s: 1,
peers:{
[creatorSnapshot.key]:true,
[guestSnapshot.key]:true
}
});
match.then(()=>{
// delete waiters
admin.database().ref("/users/"+creatorSnapshot.key+"/profile/cm").set(match.key);
admin.database().ref("/users/"+guestSnapshot.key+"/profile/cm").set(match.key);
creatorSnapshot.ref.remove();
guestSnapshot.ref.remove();
return null;
}).catch((error) => {
console.log(error);
});
// var matchId = boardId + "-" + creatorSnapshot.key + "-" + guestSnapshot.key;
// return admin.database().ref("/matches/"+matchId).set({
// s: 1,
// peers:{
// [creatorSnapshot.key]:true,
// [guestSnapshot.key]:true
// }
// }).then(()=>{
// // delete waiters
// // creatorSnapshot.ref.remove();
// // guestSnapshot.ref.remove();
// return null;
// }).catch((error) => {
// console.log(error);
// });
}
return null;
}).catch((error) => {
console.log(error);
});
});
Related
I'm creating a guest list program that stores the guest list in Firebase RTDB and when I check people in and out my function runs several times more than it is supposed to. I've sent alerts to the console so I know how many times it has run. I have separate functions for both check in and check out operations so it may be that I am calling my db too many times?
//-------------------- Check In and Check In Helper Functions -------------------------
//Helper Function to Grab current List index
function printArray() {
var ref = database.ref('guestList')
ref.on('value', readData, errData);
}
function readData(data){
guestList=[];
var scores = data.val();
var keys = Object.keys(scores)
for (var i=0; i < keys.length; i++){
var k = keys[i]
var name = scores[k].name;
var inside = scores[k].Inside;
var timeIn = scores[k].TimeIn;
var timeOut = scores[k].TimeOut;
guestList[i] = {
name: name,
Inside: inside,
TimeIn: timeIn,
TimeOut: timeOut,
}
}
checkIn(guestList);
}
function errData(err){
console.log('Error!');
console.log(err);
}
//Helper Function to set text box to selected name
function checkInn(name){
console.log(name)
document.getElementById('checkIn').value = name;
}
//Check in
function checkIn(list) {
//Grabs current guest to be added or deleted from form text box
var name = document.getElementById('checkIn').value;
//Checks to see if user is in list of guests and isn't in the list of guest in the party
var guestsRef = firebase.database().ref("guestList/");
guestsRef.orderByChild("name").on("child_added", function(data) {
if (name == data.val().name) {
objIndex = list.findIndex((obj => obj.name == name));
guestsRef = firebase.database().ref("guestList/" + objIndex)
guestsRef.update({
Inside: "Yes",
TimeIn: getTime(),
})
guestsRef.off();
document.getElementById('checkIn').value = "";
alerts(name, true)
}
})
}
//------------------------- Check Out ------------------------------------------------------------
//Helper Function to Grab current List index
function printArrayy() {
var ref = database.ref('guestList')
ref.on('value', readOutData, errData);
}
function readOutData(data){
guestList=[];
var scores = data.val();
var keys = Object.keys(scores)
for (var i=0; i < keys.length; i++){
var k = keys[i]
var name = scores[k].name;
var inside = scores[k].Inside;
var timeIn = scores[k].TimeIn;
var timeOut = scores[k].TimeOut;
guestList[i] = {
name: name,
Inside: inside,
TimeIn: timeIn,
TimeOut: timeOut,
}
}
checkOut(guestList);
}
//Helper Function to set text box to selected name
function checkOutt(name){
console.log(name);
document.getElementById('checkOut').value = name;
}
//Check Out
function checkOut(list) {
//Grabs current guest to be added or deleted from form text box
var name = document.getElementById('checkOut').value;
//Checks to see if user is in list of guests and isn't in the list of guest in the party
var guestsRef = firebase.database().ref("guestList/");
guestsRef.orderByChild("name").on("child_added", function(data) {
if (name == data.val().name) {
objIndex = list.findIndex((obj => obj.name == name));
guestsRef = firebase.database().ref("guestList/" + objIndex)
guestsRef.update({
Inside: "No",
TimeOut: getTime(),
})
document.getElementById('checkOut').value = "";
guestsRef.off();
alerts(name, false)
}
})
}
//Placeholder to alert user when a succesful check in or check out function runs
function alerts(name, Boolean){
if(Boolean){
console.log(name + " has been checked in!")
}
else{
console.log(name + " has been checked out!")
}
}
Here is the screenshot of my output. Thanks in advance!
Edit: Forgot to mention and apologize for my excessive use of helper functions! My HTML form calls printArrayy() and printArray first for each function!
have you tried once instep on, i mean:
ref.once('value', readOutData, errData); }
I'm trying to get some for Loops running inside a google cloud functions everytime I delete my /users node.
This is the code I'm using
exports.deleteUserAssets = functions.database.ref('/users/{userId}').onWrite((change, context) => {
const beforeData = change.before.val();
const afterData = change.after.val();
const userBuildings = Object.keys(beforeData.isAdmin); // get the buildings of the user stored in the user/userId/isAdmin node .. so far so good
const userId = beforeData.userIDforCloudFunctions; // I'm getting this from a /users/userid/userIDforCloudFucntions node ...so far so good (i've been logging it to confirm)
// making sure it was a delete operation ... so far so good
if (afterData !== null) {
return 0;
}
else {
// on each building
for (var i = 0; i < userBuildings.length; i++) {
let eachBuilding = [userBuildings[i]]
// HERE IS WERE THE PROBLEM IS: Trying to delete all depts + rooms + doors
admin.database().ref('/buildings/' + eachBuilding)
.child("hasDepts")
.once("value")
.then(function(snapshot) { // This is where it goes south – snapshot is returning null
snapshot.forEach(function(childSnapshot) {
var deptKeyString = childSnapshot.key; // will try to get the keys of the departments stored under this space
var deptsOnNode = admin.database().ref('/depts/' + deptKeyString);
deptsOnNode.remove(); // and use the keys to delete each of the depts on depts
});
});
admin.database().ref('/buildings/' + eachBuilding).set({}); // this is working
admin.database().ref('/buildingsUserUid/' + userId + '/' + eachBuilding).remove(); // this is working
}
}
return 0;
});
The snapshot of admin.database().ref('/buildings/' + eachBuilding).child("hasDepts") is returning null.
How can I get to it? Besides admin.database().ref() I've tried to reach it with firebase.database().ref() which is the command/object i use to get this running on frontend functions. I've also tried functions.database() with no result.
Taking in consideration what Doug Stevenson mentioned in his second comment:
exports.deleteUserAssets = functions.database.ref('/users/{userId}').onDelete((change, context, event) => {
const beforeData = change.before.val(); // data before the write (data of all the doors child nodes)
const afterData = change.after.val(); // data before the write (data of all the doors child nodes)
const userBuildings = Object.keys(beforeData.isAdmin); // get the buildings of the user
const userId = beforeData.userIDforCloudFunctions;
// make sure user was deleted
if (afterData !== null) {
return 0;
}
else {
// on each building
for (var i = 0; i < userBuildings.length; i++) {
let eachBuilding = [userBuildings[i]]
// Need to RETURN the whole chain of promises
return admin.database().ref('/buildings/' + eachBuilding)
.child("hasDepts")
.once("value")
.then(function(snapshot) {
console.log(snapshot.val()) // this now works
snapshot.forEach(function(childSnapshot) {
console.log(childSnapshot.val()) // this works as well
var deptKeyString = childSnapshot.key; // get the keys of the departments stored under this space
var deptsOnNode = admin.database().ref('/depts/' + deptKeyString);
// and you can keep on going deeper if you return promises
return deptsOnNode
.child('hasRooms')
.once('value')
.then(function(grandchildSnapshot){
console.log(grandchildSnapshot.val())
grandchildSnapshot.forEach(function(grandGrandchildSnapshot){
var roomKeyString = grandGrandchildSnapshot.key;
var roomsOnDepts = admin.database().ref('/rooms/' + roomKeyString);
admin.database().ref('/roomOwners/' + userId + '/' + roomKeyString).remove();
// and return again here...
return roomsOnDepts
.child('hasDoors')
.once('value')
.then(function(grandgrandGrandchildSnapshot){
grandgrandGrandchildSnapshot.forEach(function(grandgrandGrandchildSnapshot){
var doorKeyString = grandgrandGrandchildSnapshot.key;
var doorsOnRooms = admin.database().ref('/doors/' + doorKeyString);
doorsOnRooms.remove();
let clipOwners = admin.database().ref('/clipOwners/' + doorKeyString);
clipOwners.remove();
})
roomsOnDepts.remove();
})
})
deptsOnNode.remove(); // use the keys to delete the depts on depts main Node
})
});
admin.database().ref('/buildings/' + eachBuilding).set({});
admin.database().ref('/buildingsUserUid/' + userId + '/' + eachBuilding).remove();
});
}
}
return 0;
});
im pretty new to node.js/mongoose and i run into some difficulties getting my code to work as intended. Basically i have an Array holding some id's in a sorted order and i want to check against my Reservations if they hold those id's on a certain condition (same reservation date, time slot, etc.)
I cant seem to get my logic to work that basically should take following approach: Run through the table array from index 0 to index length-1, for each index there should be a findOne({]) query, if no reservation exists with that table id, a reservation shall be placed and exit the loop, if a reservation exists go to the next index and repeat until the array is iterated through from 0...n (the specific order the array is sorted in is important here, it should always prefer the least matching). If no table id is "free" to make a new reservation it should send a response message that every table is reserved already. Anyone that could help me out on my problem? Would appreciate any hints!
app.post('/api/reservations', (req, res) => {
const resDate = new Date(req.body.reservationDate).toISOString();
const queryDate = new Date(req.body.reservationDate);
const tableDict = req.body.tableDict;
const slot = req.body.timeSlot;
const seats = req.body.seats;
const restaurant = req.body.restaurant;
const customer = req.body.customer;
const comment = req.body.comment;
function gteQueryDate(date) {
return date.toISOString();
}
function lteQueryDate(date) {
date.setDate(date.getDate() + 1);
return date.toISOString();
}
const gteDate = gteQueryDate(queryDate);
const lteDate = lteQueryDate(queryDate);
const sortedTables = [];
for (i = 0; i < tableDict[slot].length - 1; i++) {
if (tableDict[slot][i] !== null && tableDict[slot][i].seats >= seats) {
sortedTables.push([tableDict[slot][i]._id, tableDict[slot][i].seats]);
}
}
sortedTables.sort(function(a, b) {
return a[1] - b[1];
});
var createReservation = function(tableId) {
console.log("called function with id : " + tableId);
Reservation.findOne({
'restaurant': restaurant,
'timeSlot.slot': slot,
'timeSlot.table': tableId,
'reservationDate': {
'$gte': gteDate,
'$lte': lteDate
}
})
.then(reservation => {
if (!reservation) {
console.log("can create reservation with id: " + tableId);
var timeSlot = [{
'slot': slot,
'table': tableId
}];
const newReservation = new Reservation({
seats: seats,
comment: comment,
timeSlot: timeSlot,
customer: customer,
restaurant: restaurant,
reservationDate: resDate
});
newReservation.save()
.then(result => {
res.send(result);
})
.catch(err => {
console.log(err);
});
} else {
console.log("reservation with id: " + tableId + " exists already.");
}
})
.catch(err => {
console.log(err);
});
}
for (j = 0; j < sortedTables.length; j++) {
var tableId = sortedTables[j][0];
createReservation(tableId);
}
});
I have a callable function that should return a value, but the only thing ever returned is null. Below is the current version of the function. I have also tried having a return on the first promise (the original once call), and at the end in another then returning the GUID. It actually returned data in that case, but it returned immediately and the GUID was empty.
How can I accomplish my goal and still return the GUID? I don't know when the function is called if I will use a new GUID that I generate, or one that already exists in the database.
There is a similar question here: Receiving returned data from firebase callable functions , but in that case it was because he never returned a promise from the function. I am returning a promise on all code paths. Unless I have to return the initial promise from the once call? In which case, how can I return the GUID when I don't know it yet?
I am also trying to throw an error in a couple of places and the error shows up in the logs for the function, but is never sent to the client that called the function.
I am going off of the examples here: https://firebase.google.com/docs/functions/callable
Sorry for the code bomb.
Calling the function:
var newGame = firebase.functions().httpsCallable('findCreateGame');
newGame({}).then(function(result) {
// Read result of the Cloud Function.
//var sGameID = result.data.guid;
console.log(result);
}).catch(function(error) {
console.log(error);
});
Function:
exports.findCreateGame = functions.https.onCall((data, context) => {
console.log("findCurrentGame Called.")
/**
* WHAT NEEDS DONE
*
*
* Pull in user's information
* Determine their win/loss ratio and search for a game using transactions in either low medium or high queue
* If there are no open games in their bracket, search the one above, then below
* If no open games anywhere, create a new game in their bracket
* If an open game is found, write the UID to the game and add the game's ID to the user's profile
*
*/
var uid = context.auth.uid;
var section = "";
var sUsername = "";
var sProfilePic = "";
var currentGames = null;
var sGUID = "";
//Get the user's info
var userref = admin.database().ref('users/' + uid);
userref.once("value", function(data) {
var ratio = 0;
var wins = parseInt(data.val().wins);
var losses = parseInt(data.val().losses);
var lives = parseInt(data.val().lives);
if (lives < 1){
//This user is out of lives, should not have been able to get here
//Throw an exception so that we can see why it failed
throw new functions.https.HttpsError('permission-denied', 'You do not have enough lives to start a new game.');
}
sUsername = data.val().username;
sProfilePic = data.val().profilepicture;
//Handle if they have no losses
if (losses == 0){
ratio = 100;
} else {
ratio = (wins / losses) * 100;
}
//If they have played less than 5 games, put them in noob tier
if (wins + losses < 5){
ratio = 0;
}
if (ratio <= 33){
section = "noob";
} else if (ratio > 33 && ratio <= 66){
section = "average";
} else {
section = "expert";
}
}).then(() => {
//Get all of the games this user is currently in
admin.database().ref('games').orderByChild(uid).once('value', function(data) {
currentGames = data.val();
}).then(() => {
//Generate a new GUID in case we need to set up a new game
sGUID = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
var queueref = admin.database().ref('gamequeue/' + section);
queueref.transaction(function(currentGUID) {
if (currentGUID == null){
//Write our GUID in the queue
return sGUID;
} else {
//Get the id of the game we just got
sGUID = currentGUID
return null;
}
}).then((res) => {
if (res.snapshot.val() != null){
//This means we are creating the game lobby
//Generate a new answer
var newAnswer = "";
while (newAnswer.length < 4){
var temp = Math.floor(Math.random() * 9) + 1;
temp = temp.toString();
if (!newAnswer.includes(temp)){
newAnswer += temp;
}
}
var obj = {username: sUsername, score: 0, profilepicture: sProfilePic};
return admin.database().ref('games/' + sGUID).set({id: sGUID, requestor: uid, [uid]: obj, answer: newAnswer, turn: uid, status: 'pending'}).then(() => {
return {guid: sGUID};
});
} else {
//We found a game to join
//If we are in a duplicate request situation, make sure the GUID is a string
if (typeof(sGUID) != 'string'){
sGUID = Object.keys(sGUID)[0];
}
//Make sure we didn't find our own game request
if (currentGames[sGUID] != null){
//Add this GUID back to the queue, we shouldn't have removed it
return admin.database().ref('gamequeue/' + section + '/' + sGUID).set('');
//Throw an exception that says you can only have one open game at a time
throw new functions.https.HttpsError('already-exists', 'We are still finding a match for your last request. You are only allowed one open request at a time.');
} else {
//Get the current game info
admin.database().ref('games/' + sGUID).once('value', function(data) {
var sRequestor = data.val().requestor;
var sOpponentUsername = data.val()[sRequestor].username;
var sOpponentProfilePic = data.val()[sRequestor].profilepicture;
//Write all of our info to the game
return admin.database().ref('games/' + sGUID).update({[sRequestor]: {opponentusername: sUsername, opponentprofilepicture: sProfilePic}, [uid]: {username: sUsername, score: 0, opponentusername: sOpponentUsername, opponentprofilepicture: sOpponentProfilePic}, status: 'active'}).then(() => {
return {guid: sGUID};
});
});
}
}
});
});
})
});
The documentation for callable functions explains:
To return data after an asynchronous operation, return a promise. The
data returned by the promise is sent back to the client.
You have many asynchronous operations that must be chained together. A return needs to be added to each of these statements (as shown):
return userref.once("value", function(data) {...
return admin.database().ref('games').orderByChild(uid).once('value', function(data) {...
return queueref.transaction(function(currentGUID) {...
return admin.database().ref('games/' + sGUID).once('value', function(data) {...
I am working on small idea to collect errors from pages and to store them in DB and then use graph API to display information visually.
There is 8 sites and on each of them there is 100 entries - so 800 transactions per time.
I loop through each site and then sub-loop through table of errors and collect them.
I got it working if I make insert query on each of those sub-loops for all 800 entries but I am getting some sort of memory leak from so many transactions and after few minutes - Node breaks due to memory exceeding.
So I tried queuing all 800 entries into Array of Arrays and then performing multi-insert at the end of every iteration but I am getting ER_PARSE_ERROR.
var tabletojson = require('tabletojson');
var mysql = require("mysql");
var striptag = require("striptags");
var fs = require("fs");
var path = require('path');
var startCollector;
var iterations = 0;
var insertions = 0;
var duplicated = 0;
var datas = [];
var clients = ["ClientA", "ClientB", "ClientC", "ClientD", "ClientE", "ClientF", "ClientG", "ClientH"];
var appDir = path.dirname(require.main.filename);
var errorList = ["err1", "err2", "err3", "err4", "err5", "err6"];
var con = mysql.createPool({
host: "localhost",
user: "User",
password: "Password",
database: "errors"
});
function CollectErrors() {
startCollector = new Date();
for(var a = 0; a < clients.length; a++) {
(function(a) {
tabletojson.convertUrl("http://example.com" + clients[a] + "/page.php?limit=100", { stripHtmlFromCells: false }, function(response) {
var rs = response[0];
for(var l = rs.length-1; l > -1; l--) {
var newDate = formatDate(striptag(rs[l]["Date"]), striptag(rs[l]["Time"]));
var user = getUser(striptag(rs[l]["User"]));
var msg = striptag(rs[l]["Error"]);
var splitError = rs[l]["Error"].split("<a href=\"");
var link = getUrl(splitError[1]);
var id = getId(link);
var type = getType(striptag(splitError[0]));
var temp = [newDate, link, type, user, clients[a], id, msg];
datas.push(temp);
}
});
})(a);
}
con.getConnection(function(err, connection) {
connection.query("INSERT IGNORE INTO entries (time, url, type, author, client, uid, message) VALUES ?", [datas], function(err, rows) {
console.log(err);
});
connection.release();
datas = [];
});
setTimeout(CollectErrors, 10000);
}
function formatDate(date, time) {
var newdate = date.split("/").reverse().join("-");
var newtime = time+":00";
return newdate + " " + newtime;
}
function getUrl(uri) {
return "http://example.com/"+uri.split("\">Details")[0];
}
function getId(url) {
return decodeURIComponent((new RegExp('[?|&]' + "id" + '=' + '([^&;]+?)(&|#|;|$)').exec(url) || [null, ''])[1].replace(/\+/g, '%20')) || null;
}
function getType(error) {
for(var a = 0; a < errorList.length; a++) {
if(error.indexOf(errorList[a]) !== -1) {
return errorList[a];
}
}
return "Other";
}
function getUser(user) {
if(user == "" || user == " " || user == null) {
return "System";
}
return user;
}
CollectErrors();
I've tried mysql.createConnection too but that also gave me same issue.
I've been stuck for past 12 hours and I can't see what's wrong, I've even tried populating Datas table with just strings but got same error.
I've changed your code to use ES6 and correct modules features.
Useful links: correct pooling with mysql, correct insert query, async/await, IIFE, enhanced object
const tabletojson = require('tabletojson'),
mysql = require("mysql"),
striptag = require("striptags"),
fs = require("fs"),
path = require('path');
const startCollector,
iterations = 0,
insertions = 0,
duplicated = 0;
let datas = [];
const clients = ["ClientA", "ClientB", "ClientC", "ClientD", "ClientE", "ClientF", "ClientG", "ClientH"];
const appDir = path.dirname(require.main.filename);
const errorList = ["err1", "err2", "err3", "err4", "err5", "err6"];
const con = mysql.createPool({
host: "localhost",
user: "User",
password: "Password",
database: "errors"
});
// We'll use async/await from ES6
const collectErrors = async() => {
// Up to here I've only changed syntax to ES6
let startCollector = new Date();
// We'll try to iterate through each client. And we use here for..of syntax to allow us using await
for (let client of clients) {
// Please, check that client value return correct data. If not, change for..of to your for..each and client variable to clients[a]
const tbj = await tabletojson.convertUrl("http://example.com" + client + "/page.php?limit=100", {
stripHtmlFromCells: false
});
const result = tgj[0];
for (rs of result) {
// I can't check this part, but I hope your example was with correct values.
let newDate = formatDate(striptag(rs[l]["Date"]), striptag(rs[l]["Time"]));
let user = getUser(striptag(rs[l]["User"]));
let link = getUrl(splitError[1]);
let msg = striptag(rs[l]["Error"]);
let id = getId(link);
let splitError = rs[l]["Error"].split("<a href=\"");
let getType = getType(striptag(splitError[0]));
// ES6 enhanced object syntax
datas.push({
newDate,
user,
msg,
id,
splitError,
link,
getType,
temp: [newDate, link, type, user, client, id, msg]
});
}
}
// OK, here we have fulfilled datas array. And we want to save it.
con.getConnection((err, connection) => {
// Please, notice, here I've changed your insert query to prepared statement.
connection.query("INSERT IGNORE INTO entries SET ?", datas, (err, rows) => {
console.log(err);
connection.release();
datas = [];
});
});
// I don't see why do you need timeout here, so I've left it commented.
// setTimeout(CollectErrors, 10000);
};
// Here your other methods go....
// And to call your async function we'll use IIFE
(async() => {
await collectErrors();
})();
Probably there may be errors with mysql insert, but that's not for sure. If occurred, please write in comments and I'll help you with that.