How to stop reading child_added with Firebase cloud functions? - javascript

I try to get all 10 records using this:
exports.checkchanges = functions.database.ref('school/{class}').onCreate(snap => {
const class=snap.params.class;
var ref = admin.database().ref('/students')
return ref.orderByChild(class).startAt('-').on("child_added", function(snapshot) {
const age=snapshot.child("age");
// do the thing
})
)}
The problem is that after I get the 10 records I need correctly, even after few days when a new record is added meeting those terms, this function is still invoked.
When I change on("child_added to once("child_added I get only 1 record instead of 10. And when I change on("child_added to on("value I get null on this:
const age=snapshot.child("age");
So how can I prevent the function from being invoked for future changes?

When you implement database interactions in Cloud Functions, it is important to have a deterministic end condition. Otherwise the Cloud Functions environment doesn't know when your code is done, and it may either kill it too soon, or keep it running (and thus billing you) longer than is necessary.
The problem with your code is that you attach a listener with on and then never remove it. In addition (since on() doesn't return a promise), Cloud Functions doesn't know that you're done. The result is that your on() listener may live indefinitely.
That's why in most Cloud Functions that use the Realtime Database, you'll see them using once(). To get all children with a once(), we'll listen for the value event:
exports.checkchanges = functions.database.ref('school/{class}').onCreate(snap => {
const class=snap.params.class;
var ref = admin.database().ref('/students')
return ref.orderByChild(class).startAt('-').limitToFirst(10).once("value", function(snapshot) {
snapshot.forEach(function(child) {
const age=child.child("age");
// do the thing
});
})
)}
I added a limitToFirst(10), since you indicated that you only need 10 children.

Related

Is there a way to get the actual timestamp when using firebase.database.ServerValue.TIMESTAMP?

This question is about the javascript client.
I have code that goes something like this:
const localEvents = [];
const fbEvents = firebase.database().ref("myevents");
fbEvents.on("child_added", function(snapshot) {
const e = snapshot.val();
localEvents.push(e);
});
function createEvent(e) {
e.time = firebase.database.ServerValue.TIMESTAMP;
fbEvents.push(e);
}
After calling createEvent({}), it appears that entries in my localEvents list have time values which are not equal to the actual entries in the database (the client guesses the timestamp and calls the child_added handler before it's actually done a roundtrip to the server). Is there any way to avoid this, and/or is there any way to get a callback when the actual value of the time is known?
It's not possible, using only the snapshot in the listener, to determine if the timestamp comes from the server or is guessed locally.
What you can do instead is use the promise returned from fbEvents.push(e) to determine when the write actually succeeds. A resolved promise which means it was definitely written to Firebase. The listener callback you get after that will contain the server's updated value.
(Note that with Firestore it is possible to determine if a document was fully written to the server or not. Just not with Realtime Database.)

How to connect socket.io clients using a loop

I have a number of socket.io servers serving on iterative ports (port 4001, 4002, 4003, etc)
I want to connect each socket client to the corresponding servers using a loop:
connectSockets = (sensors) => {
const responses = {};
for (const [idx, sensor] of sensors.entries()) {
const socket = socketIOClient(`${endpointBase}:${port + idx}`);
socket.on(`From::${sensor}`, data => {
responses[sensor] = data
});
}
this.setState({
responses
});
};
When I break inside the loop on:
responses[sensor] = data
I can see the data. It is even getting assigned to the appropriate "responses" property.
However, when I get out of the loop and break in setState I see:
responses = {}
No idea why. Scoping issue of some kind? Or maybe I am confused as to how socket.io works - first time using it. Interestingly when I break on "const socket" i get three iterations as I expect, but when I break on "responses[sensor] = data" I get a lot more than 3 iterations. Anyone have any ideas?
Well I figured it out on my own. As it turns out it was a mis-understanding as to how socket.io works. As t.niese mentions the callback is async.
What i needed to do was set the state within the socket connection since "data" will not necessarily be available when the loop completes:
socket.on(`From::${sensor}`, data => {
responses[sensor] = data
this.setState({responses});
});
Worked perfectly.
It is not as you assumed in your question (or your answer) as scoping issue, but related to when certain pats of the code are execute. If you place some console.log (or breakpoints in your code) it will become obvious:
connectSockets = (sensors) => {
const responses = {};
console.log('before loop')
for (const [idx, sensor] of sensors.entries()) {
const socket = socketIOClient(`${endpointBase}:${port + idx}`);
socket.on(`From::${sensor}`, data => {
console.log(`socket.on callback for: ${sensor}`)
responses[sensor] = data
});
}
console.log('after loop')
this.setState({responses});
};
The output of in the console, or the order in which those breakpoints are reach is:
before loop
after loop
socket.on callback for: ...
...
socket.on callback for: ...
The event handlers registered with .on are called at the time when an even happens, but because JavaScript is not multi threaded, the event handling cannot happen at the same time when you register an event handler with .on.
You can solve that - as you already figured out - by moving the this.setState({responses}) to the event callback.Now you call this.setState multiple times. If this is a problem, depends on your setState function.
responses[sensor] = data is triggered asynchronously at a later pointer in time.
this.setState({responses}) is being called in the same event loop. This is the reason you wouldn't find it populated with anything.

Using data returned from Firebase fetch Javascript

Web dev novice here. I'm using Firebase as my backend, and I have a situation where I need to query the DB for one value and use that returned value to match in another query to the same DB.
Here is my code:
function dataLoad() {
var valueThatINeedToUseElsewhere
userReference.on('value',function(users){
var currentUser = firebase.auth().currentUser.uid
users.forEach(function(user){
if(user.key === currentUser) {
valueThatINeedToUseElsewhere = user.val().userName
console.log(valueThatINeedToUseElsewhere)
}
})
})
console.log(valueThatINeedToUseElsewhere)
}
The console.log inside the IF condition logs the correct value. However, the last console.log above does not log anything, despite the variable name being declared outside the 'on' function. Why is this happening? And how can I actually use the data outside the 'on' function? I need to use it to perform a similar IF condition in another data retrieval operation.
Thanks!
You're likely trying to do something like this now:
var valueThatINeedToUseElsewhere;
dataLoad();
doSomethingWith(valueThatINeedToUseElsewhere);
Unfortunately this doesn't work, since (as Doug says) the data is loaded from Firebase asynchronously. By the time you're trying to use valueThatINeedToUseElsewhere, the value hasn't been loaded yet.
Why this doesn't work, the one-three-two test
It's easiest to see this if you place a few log statements like this:
console.log("Before starting to load value")
userReference.on('value',function(users){
console.log("Loaded value")
})
console.log("After starting to load value")
When you run this code, it prints:
Before starting to load value
After starting to load value
Loaded value
That is probably not the order that you expected. But it perfectly explains why you can't use valueThatINeedToUseElsewhere when you try: it hasn't been loaded yet.
Reframing the problem
The best way I've found for dealing with this situation is to reframe the problem. Your current code is written with the logic of "first we load the value, then we do something with the value". For asynchronous loading it's better to frame it as "first start loading the data. once the data has started loading, do something with it".
In code that look like:
function dataLoad() {
userReference.once('value',function(users){
var currentUser = firebase.auth().currentUser.uid
users.forEach(function(user){
if(user.key === currentUser) {
var valueThatINeedToUseElsewhere = user.val().userName
doSomethingWith(valueThatINeedToUseElsewhere)
}
})
})
}
As you'll see, we've moved the code that needs the data into the callback that fires when the data is available. That way you can be guaranteed the data is available when you call doSomethingWith. I also changed the code to use once, which (as Doug said) is better for your use-case.
There are two more steps to take, both to improve the flexibility and performance of your code.
Passing in a function that is called once the data is loaded
First off: with this last update, the call to doSomethingWith is hard-coded, which means that dataLoad() and doSomethingWith are closely tied together. It's often better to keep them more separate. To do that, we can pass in a so-called callback function into dataLoad that it then calls when it has loaded the data. This is very similar to what once() does already. Let's see that in practice:
function dataLoad(callback) {
userReference.once('value',function(users){
var currentUser = firebase.auth().currentUser.uid
users.forEach(function(user){
if(user.key === currentUser) {
var valueThatINeedToUseElsewhere = user.val().userName
callback(valueThatINeedToUseElsewhere)
}
})
})
}
Not too different. But now you can invoke dataLoad like this:
dataLoad(doSomethingWith);
And with that doSomethingWith will be called with the data, once the data has been loaded.
Loading the data by its key
The final change is an optimization. Your current code is loading all users, while you only need one and know their key. It's much more efficient to only load that specific user, with:
function dataLoad(callback) {
var currentUser = firebase.auth().currentUser.uid
userReference.child(currentUser).once('value',function(users){
var valueThatINeedToUseElsewhere = user.val().userName
callback(valueThatINeedToUseElsewhere)
})
}
on() is asychronous and returns immediately, which means your console log is going to show an undefined value. The callback you pass to on() is only going to run when results are available, and there's no guarantee how quickly that will happen. You should only use a value from an asynchronous call after it finishes - don't try to make your code block until some async call is finished.
Also consider using once() instead of on() if you only need the value at a location a single time. You can use its returned promise to chain some additional work after it loads the data you want. It is also asynchronous (as is all other methods that return a promise).
To learn more about why Firebase APIs are asynchronous, and what that means for your app, read this blog.

Cloud Function stuck in an infinite loop

exports.addNewValue = functions.database.ref('/path')
.onWrite(event => {
event.data.adminRef.update({"timeSnapshot":Date.now()})})
It appears that Date.now() causes an infinite loop in the function because the following does not:
exports.addNewValue = functions.database.ref('/path')
.onWrite(event => {
event.data.adminRef.update({"timeSnapshot":"newString"})})
How do I fix this?
If you write back to the same location in the database that was previously changed, you can expect this sequence of events:
Function is triggered with the first change from the client
Function writes back to the database
Function is triggered a second time because of the write during step #2
All writes to the database that match the filter path, even those from within the same function, will trigger the function.
In step 3, you need a strategy to figure out if the second function invocation should result in yet another write back to the database. If it does not require another write, the function should return early so that it won't trigger another write. Typically you look at the data in the event passed to the function and figure out if it was already modified the first time. This could involve looking to see if some flag is set in the database, or if the data you modified does not need any more changes.
Many of the code samples provided by the Firebase team do this. In particular, look at text moderation. Also, there is a video that describes the problem and a possible solution. In the end, you're responsible for coming up with the strategy that meets your needs.
I think the following should work fine :-
exports.addNewValue = functions.database.ref('/path/timeSnapshot')
.onWrite(event => { event.data.adminRef.set(Date.now()) })
The logic behind the above is that when you put a trigger function on a higher node (such as /path in your case), then the function would be fired each time a change is made to any of its child nodes (/timestamp in your case) - hence, the infinite loop.
Therefore, as a general practice, for efficiency as well as cost effectiveness, make sure that your trigger function has the lowest possible path node. Flatting out your data really helps in this as well.
If you arrived here having problems with querying try using .once('value') ... it will mean that you only look at the reference point once ... i.e.
ref.orderByChild("isLive").equalTo(true).once("value" , function(snapshot) {
instead of
ref.orderByChild("isLive").equalTo(true).on("value", function(snapshot) {
as the second will have you listening all the time, and when data changes at the ref, the listener will receive the changes and run the code inside your block again

Child_added subscription seems to download entire dataset

I have a large data set (~100k entries), that is being subscribed to using the 'child_added' event. Using node 7 and firebase 3.6.1, doing this seems to download the entire 100k entries before a single child_added event is fired.
Memory consumption grows significantly for a few dozen seconds, and then all child_added events are fired swiftly after each other.
This is slow:
require('firebase').
initializeApp({databaseURL: 'https://someproject.firebaseio.com'}).
database().ref('data').
on('child_added', (snap) => console.log(snap.key));
Limiting is still fast (few seconds delay):
require('firebase').
initializeApp({databaseURL: 'https://someproject.firebaseio.com'}).
database().ref('data').limitToFirst(10).
on('child_added', (snap) => console.log(snap.key));
Given the streaming nature of Firebase, I assume it is not intended behaviour for child_added subscriptions to download the entire data set to the client before anything is done.
Am I doing something wrong, or is this a bug?
Although that in the child_added section extracted from firebase documentation it says:
The child_added event is typically used when retrieving a list of items from the database. Unlike value which returns the entire contents of the location, child_added is triggered once for each existing child and then again every time a new child is added to the specified path. The event callback is passed a snapshot containing the new child's data. For ordering purposes, it is also passed a second argument containing the key of the previous child.
At the first lines in that page, we can found this:
Data stored in a Firebase Realtime Database is retrieved by attaching an asynchronous listener to a database reference. The listener will be triggered once for the initial state of the data and again anytime the data changes.
Seems to be its normal behaviour. It first retrieves all the data.
I am in the same situation, waiting nearly 40 seconds for the first child to fire. The only solution I could come up with was to get the keys using Firebase rest API and shallow query parameter, then loop over each key and call Firebase. Here is basically what I did.
`
console.log('start', Date.now());
fetch('https://[firebase_app].firebaseio.com/[your_path].json?shallow=true')
.then((response) => {
return response.json();
}).then(function(j) {
Object.keys(j).forEach(function (key) {
console.log(key, 'start', Date.now());
firebase_reference.child(key).on("child_added", function (snapshot) {
console.log(key, Date.now());
//now you have the first response without waiting for everything.
});
});
});`
I know this doesn't answer your question about child_added functionality, but it does what you would expect to happen with child_added. I am going to submit a feature request to Firebase and link this SO question.

Categories