Firebase: Atomically pop random element from realtime database - javascript

My firebase realtime database begins with a small list of unique elements in arbitrary order. Users can fetch an element(s) from this list, atomically popping it from the database, such that no other user can possess the same element. Users can also return their popped element to the list. In this way, the elements currently held by users, and those left in the database, are conserved. The list is small enough (max 27 elements) that I can efficiently load the entire database contents into memory if needed.
I am struggling to express this behaviour into my web (pure javascript) firebase application. I have seen firebase transactions, but I'm not sure how to use these such that the popped child is selected psuedo-randomly.
Here is an inadequate attempt which violates atomicity (users may end up popping/getting the same element)
function popRandElem() {
// fetch all elements currently in db list
db.ref('list').get().then( (snap) => {
// choose random element
var elems = snap.val();
var keys = Object.keys(elems);
var choice = keys[ keys.length * Math.random() << 0 ];
// remove chosen element from db
db.ref('list').child(choice).remove();
return elems[choice];
}
}
myElem = popRandElem();
function restoreElem() {
db.ref('list').push(myElem);
myElem = null;
}
How can I adapt this example such that popRandElem atomically pops from the database?

This turned out to be straightforward with transactions, using the optional second callback to obtain the successfully popped element.
function popRandElemAsynch() {
var choice = null;
db.ref('list').transaction(
// repeats with updated list until run without collision
function( list ) {
// discard previous repetition choice
choice = null;
// edge-case of list emptied during transac repeats
if (!list)
return list;
// choose and remember a random element
var keys = Object.keys(list);
choice = keys[ keys.length * Math.random() << 0 ];
// remove the element
delete list[choice];
return list;
},
// runs once after above has run for final time
function() {
// choice is the final uniquely popped element
// if it is null, list was emptied before collision-free pop
someFunc(choice);
},
// don't trigger premature events from transaction retries
false
);
}

Related

JSON Parse error undefined variable u in the beggning / can not read subSubItems${i} while repeater data has values

I am kinda new to the Velo coding / javascript in general and I am trying to re-create this tutorial --> https://support.wix.com/en/article/velo-tutorial-creating-an-expanding-mega-menu
Without the second menu (only a main menu and a strip with the items like attached screenshot).
I have a problem with javascript and wix coding. Even though I have created the collections correctly for some reason I can not open the site menu correctly. I am also attaching a wix forum link: https://www.wix.com/velo/forum/coding-with-velo/typeerror-cannot-read-properties-of-undefined-reading-filter.
Can not read the data subSubItems{i} from repeaterData, while it has all the necessary data in it
I am also attaching a screenshot of the database inself for review. (Json format on subSubItems) and the wix site itself as of right now: https://giannisliko.wixsite.com/my-site-1
The global page coding is this:
// The code in this file will load on every page of your site
//-------------Imports-------------//
import wixData from 'wix-data';
//-------------Global Variables-------------//
//Number of Submenu 2 repeaters.
const subLevel2RepeaterCount = 5;
//Object containing all menu data from subTitles database collection.
let menuData;
$w.onReady(async () => {
//Get the menu data from the collection.
menuData = await wixData.query("SubTitlesCollection").find().then(result => result.items);
//console.log(menuData);
//Set up each Submenu 2 repeater as it is loaded.
for (let i = 1; i <= subLevel2RepeaterCount; i++) {
$w(`#repeaterSubSub${i}`).onItemReady(($item, itemData, index) => {
//Get the repeater button from its ID.
const repeaterButton = $item(`#buttonSubLevelTwo${i}`)
//Set the item label.
repeaterButton.label = itemData.label;
//Set the item link.
repeaterButton.link = itemData.url;
});
}
});
export function buttonMainMenu_mouseIn(event) {
//Get the ID of the Submenu 1 button the mouse hovers over.
const selectedRootId = event.context.itemId;
//Get all the data of the Submenu 2 related to Submenu 1.
const repeaterData = menuData.filter(item => item.menu === selectedRootId);
const repeaterData2 = menuData.filter(item => item._id === selectedRootId);
console.log(repeaterData2);
//Set up the box element corresponding to the selected button in Submenu 2.
setSubSubMenu(repeaterData);
//Show the Submenu 2 box.
$w('#megaMenuStrip').expand();
}
export function repeaterMainMenu_mouseOut(event) {
}
function createUniqueId() {
//Creating a Unique Id for each of the menu sub-items by getting the current millisecond and adding a random number from 1 to 1000
let id = String(+new Date() + Math.floor(Math.random() * 1000))
return id;
}
function setSubSubMenu(repeaterData) {
//Set the image of the Submenu 1
//$w('#rangeMenuImage').src = repeaterData.img1;
for (let i = 1; i <= subLevel2RepeaterCount; i++) {
//Convert the Submenu 2 string to a Javascript object.
console.log(repeaterData);
console.log(repeaterData[`subSubItems1`]);
const dataSubSub = JSON.parse(repeaterData[`subSubItems${i}`]);
//Set a unique ID for each item.
console.log(dataSubSub);
dataSubSub.forEach(subSubItem => {
subSubItem._id = createUniqueId();
})
//Set the Submenu 2 data in the repeater.
$w(`#repeaterSubSub${i}`).data = dataSubSub;
}
}
export function megaMenuStrip_mouseOut(event) {
$w('#megaMenuStrip').collapse();
}
/**
* Adds an event handler that runs when the mouse pointer is moved
onto the element.
You can also [define an event handler using the Properties and Events panel](https://support.wix.com/en/article/velo-reacting-to-user-actions-using-events).
[Read more](https://www.wix.com/corvid/reference/$w.Element.html#onMouseIn)
* #param {MouseEvent} event
*/
/**
* Sets the function that runs when a new repeated item is created.
[Read more](https://www.wix.com/corvid/reference/$w.Repeater.html#onItemReady)
* #param {$w} $item
*/
Thank you very much in advance
I can't say for sure what's going on, but I can try to point you in the right direction.
If repeaterData is undefined, that means something is probably going wrong on the following line:
const repeaterData = menuData.filter(item => item._id === selectedRootId)[0]
The problem there could be that menuData is undefined, which would mean your query is no good.
Or it could be that none of the item IDs match the selectedRootId. I'm guessing that is the case. Looks like in the tutorial they aren't try to match with item._id like you are doing. It's really hard to tell exactly what's wrong there, but I'm pretty sure that's where your issue is. It could be a problem with your repeater IDs or it could be with the data coming from the collection. Either way, you're not getting any matches there.

Discord.js Role Assign and Looping Issues

I am not very efficient with my code which may be the reasons why this keeps failing. I am trying to remove and assign roles to "verified" users. The basic gist of the code is to loop through all "verified" users and assign them appropriate roles according to the data received from the API.
const fetch = require("node-fetch");
var i = 0;
function mainLoop(
guild,
redisClient,
users,
main_list,
Pilot,
Astronaut,
Cadet,
main_guild,
cadet_guild,
guest
) {
setTimeout(function () {
redisClient.GET(users[i], async function (err, reply) {
if (reply != null) {
var json = await JSON.parse(reply);
var uuid = Object.keys(json).shift();
if (Object.keys(main_list).includes(uuid)) {
var tag = users.shift();
var rank = main_list[uuid];
console.log(`${tag}: ${rank}`);
var role = guild.roles.cache.find(
(role) => role.name === `| ✧ | ${rank} | ✧ |`
);
await guild.members.cache.get(tag).roles.remove(guest);
await guild.members.cache.get(tag).roles.remove(Astronaut);
await guild.members.cache.get(tag).roles.remove(Cadet);
await guild.members.cache.get(tag).roles.remove(Pilot);
await guild.members.cache.get(tag).roles.remove(cadet_guild);
await guild.members.cache.get(tag).roles.add(main_guild);
await guild.members.cache.get(tag).roles.add(role);
} else {
var tag = users.shift();
console.log(`${tag}: Guest`);
await guild.members.cache.get(tag).roles.remove(Astronaut);
await guild.members.cache.get(tag).roles.remove(Cadet);
await guild.members.cache.get(tag).roles.remove(Pilot);
await guild.members.cache.get(tag).roles.remove(main_guild);
await guild.members.cache.get(tag).roles.remove(cadet_guild);
await guild.members.cache.get(tag).roles.add(guest);
}
}
i++;
if (i < users.length) {
mainLoop(
guild,
redisClient,
users,
main_list,
Pilot,
Astronaut,
Cadet,
main_guild,
cadet_guild,
guest
);
}
});
}, 5000);
}
The code will fetch api data, map the "verified" users and api data into an array. Then, when it starts looping through the users array, it will only log 3 times and not assign any roles. Any help would be appreciated.
I can provide extra explanation/code if needed.
One possible issue I see here is that you are both incrementing the index i and calling .shift() on the users array. This may be the cause of the problem you are experiencing, as this will entirely skip some of the users in the array. Array.shift() doesn't just return the first element of the array; it removes it from the array.
Consider, for example, that your users array looks like this:
var users = ["Ted", "Chris", "Ava", "Madison", "Jay"];
And your index starts at 0 like so:
var i = 0;
This is what is happening in your code:
Assign roles for users[i]; the index is currently 0, so get users[0] (Ted).
Get Ted's tag via users.shift(). users is now: ["Chris", "Ava", "Madison", "Jay"]
Increment the index with i++. i is now: 1.
Assign roles for users[i]; the index is currently 1, so get users[1] (now Ava, skips Chris entirely).
Get Ava's tag via users.shift() (actually gets Chris' tag). users is now: ["Ava", "Madison", "Jay"]
Increment the index with i++. i is now: 2.
Assign roles for users[i]; the index is currently 2, so get users[2] (now Jay, skips Madison entirely).
And so on, for the rest of the array; about half of the users in the users array will be skipped.
I don't know how many users are supposed to be in your users array, but this could be the reason why so few logs are occurring. Note, however, that this is just one cause of the problem you are experiencing; it is possible that there are more reasons why you are having that issue, such as rate limits.
My recommendation on how to fix this is to not use users.shift() to get the user's tag. Simply use users[i], which will return the proper tag value without messing with the length of the array. Another way to fix this would be to remove the index incrementation, and always use 0 as your index. Use one or the other, but not both.

Filter an array based on another array. (Using React)

The goal is to filter an array based on the slots the user has selected.
For example an array has slots for 7pm-9pm,10pm-12pm and so on.
Now the user selects 7pm-9pm, so now I want to filter the array which have 7ppm-9pm or is the users wants
7pm-9pm and 10pm-11pm so the data should be based on 7pm-9pm and 10pm-11pm
Here is how I store the values
This is the original array
data :[
{
name:"something",
phone:"another",
extraDetails : {
// some more data
slots : [
{item:"6PM-7PM"},
{item:"7PM-8pm}
]
}
},{
// Similarly more array with similar data but somewhere slots might be null
}
]
Now for example we have this array
slots:[{6PM-7PM,9PM-10PM,11PM-12AM}]
Now this should filter all those which includes timeslots of 6PM-7PM,9PM-10PM,11PM-12AM
or if the user selects
slots:[{6PM-7PM}]
We should still get the results that includes 6pm-7pm more or else don't matter.
First, I'd suggest using this for your slots representation for simplicity, but you can alter this approach depending on your actual code:
slots: ['6PM-7PM', '9PM-10PM', '11PM-12PM']
Then you can iterate through your data and use filter:
const querySlots = ['6PM-7PM', '9PM-10PM', '11PM-12PM'];
const matchedPersonsWithSlots = data.filter( (person) => {
let i = 0;
while ( i < person.extraDetails.slots.length ) {
if (querySlots.includes(person.extraDetails.slots[i]) return true;
i += 1;
}
return false;
});
matchedPersonsWithSlots will then have all the people that have a slot that matches one of the slots in your query, because if any of the query slots are in a person's list of slots, then it's included in the result set.
EDIT to include a different use case
If, however, every slot in the query array must be matched, then the filtering has to be done differently, but with even less code.
const matchedPersonsWithAllSlots = data.filter(person =>
querySlots.every((qSlot)=>person.extraDetails.slots.includes(qSlot)));
The above will go through each person in your data, and for each of them, determine whether the person has all of your query slots, and include them in the result list, only if this is true.

Firebase functions, counting elements for big data

Recently Firebase introduce Cloud Functions.
In my case this feature is very usefull to count elements in my database.
Firebase posted a sample code to count elements but I ask myself some questions with big data.
In our example we consider that we need to count likes for a post.
In the sample code, at each new like, the function count all likes for the current post and update the count.
Do you think it's a good solution for big data ? (For example if we have 1M likes)
Thank you in advance !
Agreed that the code in the functions sample is not ideal for large sets of data.
For a long time I've used a two-stepped approach in my counters:
when a child is added/removed, increase/decrease the counter
when the counter gets deleted, recount all the children (as it does now)
So case #2 is memory-bound the same as the current code. But case #1 triggers on child writes, so is a lot less memory hungry.
The code:
// Keeps track of the length of the 'likes' child list in a separate property.
exports.countlikechange = functions.database.ref("/posts/{postid}/likes/{likeid}").onWrite((event) => {
var collectionRef = event.data.ref.parent;
var countRef = collectionRef.parent.child('likes_count');
return countRef.transaction(function(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;
}
});
});
// If the number of likes gets deleted, recount the number of likes
exports.recountlikes = functions.database.ref("/posts/{postid}/likes_count").onWrite((event) => {
if (!event.data.exists()) {
var counterRef = event.data.ref;
var collectionRef = counterRef.parent.child('likes');
return collectionRef.once('value', function(messagesData) {
return counterRef.set(messagesData.numChildren());
});
}
});
I also submitted this in a PR for the repo.
See the sample of this in functions-samples.
Given a data structure similar to this:
/functions-project-12345
/posts
/key-123456
likes_count: 32
/likes
user123456: true
user456789: true
user786245: true
...
This function would do the trick:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
// Keeps track of the length of the 'likes' child list in a separate attribute.
exports.countlikes = functions.database.ref('/posts/{postid}/likes').onWrite(event => {
return event.data.ref.parent.child('likes_count').set(event.data.numChildren());
});
Note that this code is copyright Google and apache licensed. See the code for more details.

Pick random value from firebase snapshot

I'm using firebase's foreach to get each child in a tree from this url
Objective, when the page loads grab a random item from firebase and show it
data structure
grabbit (table name)
active (for active items for sale)
category (the category of the item ie womensClothes, MensShoes etc)
unique id of the item
On page load go into http://gamerholic.firebase.com/grabbit/active
and grab any one of the categories and return it..
Script
var grabbitRef = new Firebase('https://gamerholic.firebaseIO.com/grabbit/active/');
grabbitRef.on('value', function(snapshot) {
if(snapshot.val() === null) {
alert("invalid");
} else {
// get snap shot data:
snapshot.forEach(function(snapshot) {
var name = snapshot.name();
alert(name);
});
}
});
After I have a random category say "electronics", I get a new snapshot and have it return any random item that's in electronics
var grabbitRef = new Firebase('https://gamerholic.firebaseIO.com/grabbit/active/'+name);
grabbitRef.on('value', function(snapshot) {
if(snapshot.val() === null) {
alert("invalid");
} else {
// get snap shot data:
snapshot.forEach(function(snapshot) {
var id = snapshot.name();
alert(id);
});
}
});
with the id I can now get the details of the item
var grabbitRef = new Firebase('https://gamerholic.firebaseIO.com/grabbit/active/'+name+'/'+id);
It's not possible to grab a random item from the list in Firebase, unfortunately. You can do limit(1) to grab the first item, or endAt().limit(1) to grab the last item.
If you're using forEach, you can also grab all the items like you are and then picking one at random, using Math.random. For example:
var i = 0;
var rand = Math.floor(Math.random() * snapshot.numChildren());
snapshot.forEach(function(snapshot) {
if (i == rand) {
// picked random item, snapshot.val().
}
i++;
});
One way to implement this is to store an additional child attribute with each element of your list that will take on a random value. Let's call the name of this attribute "randomVal".
To pick your random entry you would use orderByChild("randomVal") and then limit the query to returning exactly one entry. Immediately upon fetching this entry, you would write a new random value into the "randomVal" element.
Beyond the additional bookkeeping work, the biggest downside of this approach is that it requires a write to the database every time you want to select a random element. I'm new to firebase, so I don't know how significant this drawback is.
Also, make sure to index this part of the database on "randomVal" to improve the query performance.

Categories