Firebase transaction()-like functionality when adding to a list? - firebase

This is a rather involved question, but I'll try to explain it as simply & concisely as I can...
I am using Firebase to build a multi-user, web-based game. I am keeping a list of every round in the game. At the end of a round, every user is presented with a "Start" button that they click when they are ready to begin the next round. The round begins when at least 50% of users have clicked "Start".
I have a Firebase reference gameRef for the game, a reference roundListRef that represents the list of rounds, and a reference roundRef that represents the current round.
I have attached a child_added callback to roundListRef so that when a new round is added, it becomes everyone's current round:
roundListRef.on('child_added', function(childSnapshot, prevChildName) {
roundRef = childSnapshot.ref();
});
I can track newRoundVotes and activePlayers, and calculate 50% easily from there. If 50% is reached, a new round is added, which triggers everyone's child_added event and the new round will begin from there...
gameRef.child('newRoundVotes').on('value', function(snapshot) {
var newRoundVotes = snapshot.val();
gameRef.child('activePlayers').once('value', function(snapshot) {
var activePlayers = snapshot.val();
if (newDriveVotes / activePlayers >= 0.5)
addNewRound();
});
});
My questions is, how do I ensure that only one new round is added, and that everyone is on the same round?
For example, say there are 10 players and 4 have already voted to start the next round. If the 6th player votes before his child_added event is triggered from the 5th player, then a round will also be added for the 6th player.
The problem is similar to .set() vs .transaction(), but no quite the same (from my understanding).
Does anyone have a solution?

I think you can likely solve this with a transaction if the round names are known ahead of time. E.g. if you just use /round/0, /round/1, /round/2, etc.
Then you could have some code like:
function addNewRound() {
var currentRound = Number(roundRef.name());
var nextRound = currentRound + 1;
// Use a transaction to try to create the next round.
roundRefList.child(nextRound).transaction(function(newRoundValue) {
if (newRoundValue == null) {
// create new round.
return { /* whatever should be stored for the round. */ };
} else {
// somebody else already created it. Do nothing.
}
});
}
Does this work for your scenario?

You could modify your thinking slightly and use a round counter as a place to track concurrency.
currentRound = 0;
currentRoundRef.on('value', function(snapshot) {
currentRound = snapshot.val();
roundRef = roundListRef.child(currentRound);
});
function addNewRound() {
currentRoundRef.transaction( function(current_value) {
if( current_value !== currentRound ) {
// the round timer has been updated by someone else
return;
}
else {
return currentRound + 1;
}
}, function(success) {
// called after our transaction succeeds or fails
if( success ) {
roundListRef.child(currentRound+1).set(...);
}
});
}

Related

i need to get the system time & store it as a child value in firebase

I need to add current system time into child data field.
I'm using TypeScript, but this might still give you and idea how you could do it.
My code uses the event.timestamp property to get date and time:
export const onWrite = functions.database.ref('/{databaseName}/{tableName}/{key}').onCreate((event) => {
let ref = event.data.ref;
let isCreate = event.eventType == 'providers/google.firebase.database/eventTypes/ref.create';
ref.once('value').then(e => {
// Cloud functions are sometimes executed with a delay and the record might not exist anymore.
if (e.val() === null) {
return;
}
if (isCreate) {
return ref.update({
'createdDateTime': event.timestamp
});
}
});
});
The created events for clients won't include this added data yet, only a later change event does.
I'm haven't investigated yet if this can be fixed (perhaps by making use of transaction).
I saw your image description and understood u want to add system time into firebase.
If you want to do you can do that by , like below
var fb_db=firebase.database().ref('treeName');
var key=fb_db.push().key;
var updatenode={}
updatenode[key+"\"]= new Date();
fb_db.update(updatenode).then(function(){
alert("Success")
})

Meteor-angular autocomplete from huge data

I have angular-meteor app that needs Material md-autocomplete from a collection with 53,296 documents with angularUtils.directives.dirPagination but this amount of data make my browser hang.
I'm publishing the collection with:
Meteor.publish('city', function (options, searchString) {
var where = {
'city_name': {
'$regex': '.*' + (searchString || '') + '.*' ,
'$options': 'i'
}
};
return City.find(where, options);
});
I subscribe with:
subscriptions: function () {
Meteor.subscribe('city');
this.register('city', Meteor.subscribe('city'));
}
and have pagination on controller :
$scope.currentPage = 1;
$scope.pageSize = 100;
$scope.sort = {city_name_sort : 1};
$scope.orderProperty = '1';
$scope.helpers({
city: function(){
return City.find({});
}
});
but it takes a long time to load and its make chrome stop working.
You already have most of the server-side searching done because your search is running inside a subscription. You should make sure that the city_name field is indexed in mongo! You should only return that field to minimize data transfer. You can also simplify your regex.
Meteor.publish('city', function (searchString) {
const re = new RegExp(searchString,'i');
const where = { city_name: { $regex: re }};
return City.find(where, {sort: {city_name: 1}, fields: {city_name: 1}});
});
What I've found helps with server-side auto-complete is:
Don't start searching until the user has typed 3 or 4 characters. This drastically narrows down the search results.
Throttle the search to only run every 500ms so that you're not sending every character to the server because then it has to keep re-executing the search. If the person is typing fast the search might only run every 2 or 3 characters.
Run the same .find() on the client that you're running on the server (instead of just querying for {}). That's just good practice since the client-side collection is the union of all subscriptions on that collection, there might be documents there that you don't want to list.
Lastly I don't know why you're subscribing twice here:
subscriptions: function () {
Meteor.subscribe('city');
this.register('city', Meteor.subscribe('city'));
}
only one of those Meteor.subscribe('city') calls is necessary.

Meteor minimongo dynamic cursor

In my client UI I have a form with differents search criterias, and I'd like to reactively update the results list. The search query is transformed into a classical minimongo selector, saved in a Session variable, and then I have observers to do things with the results:
// Think of a AirBnb-like application
// The session variable `search-query` is updated via a form
// example: Session.set('search-query', {price: {$lt: 100}});
Offers = new Meteor.Collection('offers');
Session.setDefault('search-query', {});
resultsCursor = Offers.find(Session.get('search-query'));
// I want to add and remove pins on a map
resultCursor.observe({
added: Map.addPin,
removed: Map.removePin
});
Deps.autorun(function() {
// I want to modify the cursor selector and keep the observers
// so that I would only have the diff between the old search and
// the new one
// This `modifySelector` method doesn't exist
resultsCursor.modifySelector(Session.get('search-query'));
});
How could I implement this modifySelector method on the cursor object?
Basically I think this method needs to update the compiled version of the cursor, ie the selector_f attribute, and then rerun observers (without losing the cache of the previous results). Or is there any better solution?
Edit: Some of you have misunderstood what I'm trying to do. Let me provide a complete example:
Offers = new Meteor.Collection('offers');
if (Meteor.isServer && Offers.find().count() === 0) {
for (var i = 1; i < 4; i++) {
// Inserting documents {price: 1}, {price: 2} and {price: 3}
Offers.insert({price:i})
}
}
if (Meteor.isClient) {
Session.setDefault('search-query', {price:1});
resultsCursor = Offers.find(Session.get('search-query'));
resultsCursor.observe({
added: function (doc) {
// First, this added observer is fired once with the document
// matching the default query {price: 1}
console.log('added:', doc);
}
});
setTimeout(function() {
console.log('new search query');
// Then one second later, I'd like to have my "added observer" fired
// twice with docs {price: 2} and {price: 3}.
Session.set('search-query', {});
}, 1000);
}
This doesn't solve the problem in the way you seem to be wanting to, but I think the result is still the same. If this is a solution you explicitly don't want, let me know and I can remove the answer. I just didn't want to put code in a comment.
Offers = new Meteor.Collection('offers');
Session.setDefault('search-query', {});
Template.map.pins = function() {
return Offers.find(Session.get('search-query'));
}
Template.map.placepins = function(pins) {
// use d3 or whatever to clear the map and then place all pins on the map
}
Assuming your template is something like this:
<template name="map">
{{placepins pins}}
</template>
One solution is to manually diff the old and the new cursors:
# Every time the query change, do a diff to add, move and remove pins on the screen
# Assuming that the pins order are always the same, this use a single loop of complexity
# o(n) rather than the naive loop in loop of complexity o(n^2)
Deps.autorun =>
old_pins = #pins
new_pins = []
position = 0
old_pin = undefined # This variable needs to be in the Deps.autorun scope
# This is a simple algo to implement a kind of "reactive cursor"
# Sorting is done on the server, it's important to keep the order
collection.find(Session.get('search-query'), sort: [['mark', 'desc']]).forEach (product) =>
if not old_pin?
old_pin = old_pins.shift()
while old_pin?.mark > product.mark
#removePin(old_pin)
old_pin = old_pins.shift()
if old_pin?._id == product._id
#movePin(old_pin, position++)
new_pins.push(old_pin)
old_pin = old_pins.shift()
else
newPin = #render(product, position++)
new_pins.push(newPin)
# Finish the job
if old_pin?
#removePin(old_pin)
for old_pin in old_pins
#removePin(old_pin)
#pins = new_pins
But it's a bit hacky and not so efficient. Moreover the diff logic is already implemented in minimongo so it's better to reuse it.
Perhaps an acceptable solution would be to keep track of old pins in a local collection? Something like this:
Session.setDefault('search-query', {});
var Offers = new Meteor.Collection('offers');
var OldOffers = new Meteor.Collection(null);
var addNewPin = function(offer) {
// Add a pin only if it's a new offer, and then mark it as an old offer
if (!OldOffers.findOne({_id: offer._id})) {
Map.addPin(offer);
OldOffers.insert(offer);
}
};
var removePinsExcept = function(ids) {
// Clean out the pins that no longer exist in the updated query,
// and remove them from the OldOffers collection
OldOffers.find({_id: {$nin: ids}}).forEach(function(offer) {
Map.removePin(offer);
OldOffers.remove({_id: offer._id});
});
};
Deps.autorun(function() {
var offers = Offers.find(Session.get('search-query'));
removePinsExcept(offers.map(function(offer) {
return offer._id;
}));
offers.observe({
added: addNewPin,
removed: Map.removePin
});
});
I'm not sure how much faster this is than your array answer, though I think it's much more readable. The thing you need to consider is whether diffing the results as the query changes is really much faster than removing all the pins and redrawing them each time. I would suspect that this might be a case of premature optimization. How often do you expect a user to change the search query, such that there will be a significant amount of overlap between the results of the old and new queries?
I have the same problem in my own hobby Meteor project.
There is filter session var where selector is storing. Triggering any checkbox or button changes filter and all UI rerender.
That solution have some cons and the main - you can't share app state with other users.
So i realized that better way is storing app state in URL.
May be it is also better in your case?
Clicking button now change URL and UI rendering based on it. I realize it with FlowRouter.
Helpful reading: Keeping App State on the URL

Firebase listen for changes in particular field

I'm working on real time virtual room with chat using firebase. I was wondering if it's possible to listen for updates on particular field in firebase db. for example if the data is structured in a following way:
{channel_name: test-app,
{id: unique_id_generated_automatically_by_firebase,
{user_id: some_id,
position: {
current: {x:x,y:y},
start: {x:x,y:y},
end: {x:x,y:y}
}
}
{id: unique_id_generated_automatically_by_firebase,
{user_id: some_id,
position: {
current: {x:x,y:y},
start: {x:x,y:y},
end: {x:x,y:y}
}
}
}
At the moment i'm able to listen for any changes in db like that
//reference to firebase db
var room = new Firebase("https://test-app.firebaseio.com/");
room.on("child_changed", function(snapshot) {
//do something here;
});
What i'm looking for is a way to listen for changes on fields position.start and position.end, but ignore position.current (those are the only fields that will get updated). The current position will be needed only when user log in to get the current positions of all users currently in the room. After that the positions will be animated on the client machine based on start and end values. I would also like to save on data transfer by not emitting changes on current position to all connected clients, but have current state when requested. Any help and suggestions much appreciated.
You can bind a .on("value") event for each field you're interested in.
var room = new Firebase("https://test-app.firebaseio.com/" + roomID);
room.child("position/start").on("value", onChange);
room.child("position/end").on("value", onChange);
function onChange(snapshot) {
if (snapshot.name() == "start") {
// position.start changed to snapshot.val()
} else if (snapshot.name() == "end") {
// position.end changed to snapshot.val()
}
}

Deactivating events with .off

Most of my .on events use closures:
ref.on( 'child_added',
function( snapshot ) {
userCallback( snapshot.val() );
} );
which means, it is not possible to deactivate these monitors with .off(), since off needs the original callback pointer as well as the eventType. How can I do this sort of thing?
My app (jQuery Mobile) is page-based. When the user hits a certain page, I want to activate monitors. When the user leaves that page, I want to de-activate, so when they re-enter, I don't end up with multiple monitors. I think I want to call .off() with only eventType and have all callbacks removed.
Right now the only option is to store a reference to the callback. We actually make this a little easier by having .on() return it back to you. So you can do:
var childCallback = ref.on('child_added', function(snapshot) { /* whatever */ });
// later...
ref.off('child_added', childCallback);
But we've received several pieces of feedback like yours, saying it is sometimes cumbersome to keep track of your callback references. So we have a planned API change so that you can call .off() with only an event type or with no arguments at all, and we'll just remove whatever callbacks are registered. But we're focused on other features right now, so this change is probably 1+ months out.
Thanks for the feedback!
In the interim, you can reproduce that sort of behavior with a simple manager pattern. For instance:
function ObserverManager( firebaseRef, page ) {
this.firebaseRef = firebaseRef;
this.listeners = {child_added: [], child_removed: [], value: [], child_updated: [], child_changed: []};
this.page = page;
}
FirebaseObservable.prototype.on(event, callback) {
this.listeners[event].push(
this.firebaseRef.on(event, function(snapshot) {
callback(snapshot.val());
})
);
}
FirebaseObservable.prototype.off(event) {
var list = this.listeners[event], i = list.length;
while(i--) { // 50% more efficient than for(i..; list.length; ...) in IE due to scoping
firebaseRef.off(event, list[i]);
}
}

Resources