I have an app where one user hosts a game, and then other users can vote on questions from the host. From the moment the host posts the question, the players have 20 seconds to vote.
How can I show a countdown timer on all player's screens and keep them synchronized with the host?
Many developers get stuck on this problem because they try to synchronize the countdown itself across all users. This is hard to keep in sync, and error prone. There is a much simpler approach however, and I've used this in many projects.
All each client needs to show its countdown timer are three fairly static pieces of information:
The time that the question was posted, which is when the timer starts.
The amount of time they need to count from that moment.
The relative offset of the client to the central timer.
We're going to use the server time of the database for the first value, the second value is just coming from the host's code, and the relative offset is a value that Firebase provides for us.
The code samples below are written in JavaScript for the web, but the same approach (and quite similar code) and be applied in iOS, Android and most other Firebase SDKs that implement realtime listeners.
Let's first write the starting time, and interval to the database. Ignoring security rules and validation, this can be as simple as:
const database = firebase.database();
const ref = database.ref("countdown");
ref.set({
startAt: ServerValue.TIMESTAMP,
seconds: 20
});
When we execute the above code, it writes the current time to the database, and that this is a 20 second countdown. Since we're writing the time with ServerValue.TIMESTAMP, the database will write the time on the server, so there's no chance if it being affected by the local time (or offset) of the host.
Now let's see how the other user's read this data. As usual with Firebase, we'll use an on() listener, which means our code is actively listening for when the data gets written:
ref.on("value", (snapshot) => {
...
});
When this ref.on(... code executes, it immediately reads the current value from the database and runs the callback. But it then also keeps listening for changes to the database, and runs the code again when another write happens.
So let's assume we're getting called with a new data snapshot for a countdown that has just started. How can we show an accurate countdown timer on all screens?
We'll first get the values from the database with:
ref.on("value", (snapshot) => {
const seconds = snapshot.val().seconds;
const startAt = snapshot.val().startAt;
...
});
We also need to estimate how much time there is between our local client, and the time on the server. The Firebase SDK estimates this time when it first connects to the server, and we can read it from .info/serverTimeOffset in the client:
const serverTimeOffset = 0;
database.ref(".info/serverTimeOffset").on("value", (snapshot) => { serverTimeOffset = snapshot.val() });
ref.on("value", (snapshot) => {
const seconds = snapshot.val().seconds;
const startAt = snapshot.val().startAt;
});
In a well running system, the serverTimeOffset is a positive value indicating our latency to the server (in milliseconds). But it may also be a negative value, if our local clock has an offset. Either way, we can use this value to show a more accurate countdown timer.
Next up we'll start an interval timer, which gets calls every 100ms or so:
const serverTimeOffset = 0;
database.ref(".info/serverTimeOffset").on("value", (snapshot) => { serverTimeOffset = snapshot.val() });
ref.on("value", (snapshot) => {
const seconds = snapshot.val().seconds;
const startAt = snapshot.val().startAt;
const interval = setInterval(() => {
...
}, 100)
});
Then every timer our interval expires, we're going to calculate the time that is left:
const serverTimeOffset = 0;
database.ref(".info/serverTimeOffset").on("value", (snapshot) => { serverTimeOffset = snapshot.val() });
ref.on("value", (snapshot) => {
const seconds = snapshot.val().seconds;
const startAt = snapshot.val().startAt;
const interval = setInterval(() => {
const timeLeft = (seconds * 1000) - (Date.now() - startAt - serverTimeOffset);
...
}, 100)
});
And then finally we log the remaining time, in a reasonable format and stop the timer if it has expired:
const serverTimeOffset = 0;
database.ref(".info/serverTimeOffset").on("value", (snapshot) => { serverTimeOffset = snapshot.val() });
ref.on("value", (snapshot) => {
const seconds = snapshot.val().seconds;
const startAt = snapshot.val().startAt;
const interval = setInterval(() => {
const timeLeft = (seconds * 1000) - (Date.now() - startAt - serverTimeOffset);
if (timeLeft < 0) {
clearInterval(interval);
console.log("0.0 left)";
}
else {
console.log(`${Math.floor(timeLeft/1000)}.${timeLeft % 1000}`);
}
}, 100)
});
There's definitely some cleanup left to do in the above code, for example when a new countdown starts while one is still in progress, but the overall approach works well and scales easily to thousands of users.
Related
I am trying to create a https function in google cloud functions that when called will make copies of a document in a Firebase database add an ID and timestamp to the copy and save it to a collection in Firestore. This function will repeat for a certain amount of time for a given interval. For example it if told to run for 2 minutes making a copy every 10 seconds, it will make 12 unique copies saved to Firestore when it is finished running.
Now I have already implemented this function for each specific document in the Firebase database. However this is not scalable. If I were to eventually have 1000 documents in the database then I would need 1000 unique functions. So I want a single https function that will make a copy of every document in the database add the unique id and time stamp to the copy before saving it to Firestore at its unique path.
Here is an example of what I have.
// Duration of function runtime in minutes
var duration = 2;
// interval in which the funtion will be called in seconds
var interval = 10;
// Make a Copy of doc 113882052 with the selected duration, interval, and runtime options
exports.makeCopy_113882052 = functions.runWith(runtimeOpts).https.onRequest((request, response) => {
var set_duration = 0; // Counter for each iteration of runEveryXSeconds
var functId = setInterval(runEveryXSeconds, (interval * 1000) ); //runEveryXSeconds will run every interval in milliseconds
// runEveryXSeconds will based on the desired parameters
function runEveryXSeconds() {
// If runEveryXSeconds has reached its last iteration clear functID so that it stops running after one last iteration
if(set_duration >= ((duration * 60 / interval) - 1)) {
console.log("have made ",((duration * 60 / interval) - 1)," copies will now exit after one last copy");
clearInterval(functId);
}
set_duration++; // Increment set_duration at the beginning of every loop
console.log("Add a charger every ", interval, " seconds until ", duration, " min is up! currently on iteration ", set_duration);
// grab a snapshot of the database and add it to firestore
admin.database().ref('/113882052').once('value').then(snapshot => {
var data = snapshot.val(); // make a copy of the snapshot
data.station_id = "113882052"; // add the station id to the copy
data.timestamp = admin.firestore.FieldValue.serverTimestamp(); // add the current server timestamp to the copy
admin.firestore().collection('copies/113882052/snapShots').add(data); // add the copy to firestore at the given path
// only return promis when all iterations are complete
if (set_duration >= (duration * 60 / interval)) {
console.log("return successfully");
response.status(200).send('Success'); // send success code if completed successfully
}
return null;
}).catch(error => {
console.log('error at promise');
response.send(error); // send error code if there was an error
return null;
});
} // End of runEveryXSeconds
}); // End of makeCopy_113882052
So this function here makes a copy of doc 113882052 from Firebase adds the station_id "113882052" and the currant time stamp to the copy and saves it to Firestore collection at the path "copies/113882052/snapShots". It does this 12 times.
All of the other docs and their associated functions work the same way, just swap out the 9 digits for a different 9.
My initial though process is to use wildcards, but those are only used with triggers like onCreate and onUpdate. I am not using these, I am using once(), so I am not sure this is possible.
I have tried swapping out the once() method with onUpdate() like so
functions.database.ref('/{charger_id}').onUpdate((change, context) => {
const before_data = change.before.val();
const after_data = change.after.val();
//const keys = Object.keys(after_data);
var data = change.after.val();
var sta_id = context.params.charger_id;
data.station_id = sta_id; // add the station id to the copy
data.timestamp = admin.firestore.FieldValue.serverTimestamp(); // add the current server timestamp to the copy
admin.firestore().collection('chargers/', sta_id, '/snapShots').add(data); // add the copy to firestore at the given path
// only return promise when all iterations are complete
if (set_duration >= (duration * 60 / interval)) {
console.log("return successfully");
response.status(200).send('Success'); // send success code if completed successfully
}
return null;
});
but it does not work. I believe it does not work because it would only make a copy if the doc was updated just as the function was called, but I am not sure.
Is there a way to use wild cards to achieve what I want, and is there another way to do this without wildcards?
Thanks!
If you want to make a copy of all nodes in the Realtime Database, you'll need to read the root node of the database.
So instead of admin.database().ref('/113882052'), you'd start with admin.database().ref(). Then in the callback/completion handler, you loop over the children of the snapshot:
admin.database().ref().once('value').then(rootSnapshot => {
rootSnapshot.forEach(snapshot => {
var data = snapshot.val(); // make a copy of the snapshot
data.station_id = snapshot.key; // add the station id to the copy
data.timestamp = admin.firestore.FieldValue.serverTimestamp(); // add the current server timestamp to the copy
...
})
Ok, I have a useInterval (custom hook) that will delete a document from firestore after a given time is milliseconds. It works fine if I set it to 10 seconds, 1 minute.
However, when I set it to delete the document a month after it was created, it looks like it creates the document and gets deleted right away.
How can I set my interval to delete the document after a month from when it was created?
const docRef = firestore.doc(`posts/${id}`)
const deleDoc = () => docRef.delete();
//******************************************************************* */
//? Deleting post after a month(time managed in milliseconds)
const now = createdAt.seconds * 1000;
const monthFromNow = 2628000000;
const then = now + monthFromNow;
const timeLeft = then - Date.now();
//?custom hook
useInterval(() => {
docRef.delete();
}, timeLeft);
You can create a task which hits one of your cloud function endpoint using Cloud Task. You can set it to hit the API every 1 min.
In the API you can pull up all the documents which are older than 1 month and delete them using the above logic.
My app is a game where a user has 30 mins to finish....node backend
Each time a user starts a game then a setInterval function is triggered server side....once 30mins is counted down then I clearInterval.
How do I make sure that each setInterval is unique to the particular user and the setInterval variable is not overwritten each time a new user starts a game? (or all setInterval's are cleared each time I clear).
Seems like I might need to create a unique "interval" variable for each new user that starts game??
Below code is triggered each time a new user starts a game
let secondsLeft = 300000;
let interval = setInterval(() => {
secondsLeft -= 1000;
if (secondsLeft === 0) {
console.log("now exit");
clearInterval(interval);
}
}, 10000);
Thanks!!
We used agenda for a pretty big strategy game backend which offers the benefit of persistence if the node app crashes etc.
We incorporated the user id into the job name and would then schedule the job, along with data to process, to run at a determined time specifying a handler to execute.
The handler would then run the job and perform the relevant tasks.
// create a unique jobname
const jobName = `${player.id}|${constants.events.game.createBuilding}`;
// define a job to run with handler
services.scheduler.define(jobName, checkCreateBuildingComplete);
// schedule it to run and pass the data
services.scheduler.schedule(at.toISOString(), jobName, {
id: id,
instance: instance,
started: when
});
Worked pretty well and offered decent protection against crashes. Maybe worth considering.
First: Concurrent Intervals and Timers are not the best design approach in JS, it is better to use one global timer and a list of objects storing the start, end, userid etc and update these in a loop.
Anyway. To have your interval id bound to a certain scope, you can use a Promise like so:
const createTimer = (duration, userid) => new Promise(res => {
const start = new Date().getTime();
let iid;
(function loop () {
const
now = new Date().getTime(),
delta = now - start
;
//elapsed
if (delta >= duration) {
clearTimeout(iid);
res(userid);
//try again later
} else {
iid = setTimeout(loop, 100)
}
})();
});
This way each timer will run »on its own«. I used setTimeout here since that wont requeue loop before it did everything it had to. It should work with setInterval as well and look like that:
const runTimer = (duration, userid, ontick) => new Promise(res => {
const
start = new Date().getTime(),
iid = setInterval(
() => {
const delta = new Date().getTime() - start;
if (delta < duration) {
//if you want to trigger something each time
ontick(delta, userid);
} else {
clearInterval(iid);
res(userid);
}
}, 500)
;
});
You do not even need a promise, a simple function will do as well, but then you have to build some solution for triggering stuff when the timer is elapsed.
Thanks #Chev and #philipp these are both good answers.
I was also made aware of a technique where you use an array for the setInterval variable.....this would make my code as follows;
let intervals = []
let secondsLeft = 300000;
intervals['i'+userId] = setInterval(() => {
secondsLeft -= 1000;
if (secondsLeft === 0) {
console.log("now exit");
clearInterval(interval);
}
}, 10000);
Does anyone else foresee this working?.
UPDATE 6.56pm PST.....it works!!
I have created a websocket (wss) client to listen to messages from a socket server. Messages are sent continuously from the server which is logged in console client side. I would like to measure the time between these messages (I know they happen once every 5 seconds but I need to calculate the time because the 5 seconds is not guaranteed).
I've thought about storing the previous message's time via Date.now() and then finding the time of the most recent messages to calculate the time difference but I'm unsure about how to do that.
socket.onmessage = (event) => {
if (event.data.substr(4, 9) === 'heartbeat') {
console.log(event.data.substr(2)) // logs msg to console
timeHeartbeats(event.data.substr(2), Date.now())
}
}
// I'm not actually keeping track of the time for 2 messages which is the issue
const timeHeartbeats = (json, mostRecent) => {
console.log(Date.now() - mostRecent) // 0 b/c Date.now() is the same as mostRecent (should be ~5000ms if time calc is correct)
}
Time since last Heartbeat:
socket.onmessage = (event) => {
if (event.data.substr(4, 9) === 'heartbeat') {
console.log(event.data.substr(2)) // logs msg to console
timeFromNow(event.data.substr(2))
}
}
// I'm not actually keeping track of the time for 2 messages which is the issue
const timeHeartbeats = (mostRecent) => (json) => {
const now = Date.now();
console.log((now - mostRecent) + ' since last heartbeat');
mostRecent = now;
}
const timeFromNow = timeHeartbeats(Date.now());
I have a simple function in Cloud Functions that is triggered when any update occurs on my "clients" location in my Cloud Firestore database.
Problem is when I update the data once it changes the lastUpdated child of the document that was updated to seconds 1572841408099 and then I change the child again, it always reverts lastUpdated to a Cloud Firestore Timestamp that is for the time: "November 3, 2019 at 9:44:16 PM UTC-6" regardless of what time it actually is.
How do I get the following code to reliably return what time the data was updated?
import * as functions from 'firebase-functions';
export const clientUpdatedTask = functions.firestore.document('clients/{clientId}').onUpdate((change: any, context: any) => {
const before = change.before.data();
const after = change.after.data();
const timeEdited = Date.now();
if(before.lastUpdated.seconds !== after.lastUpdated.seconds) {
return 'The Time Did Not Change. No Need To Run Update!';
} else {
return change.after.ref.update({'lastUpdated' : timeEdited}, {merge: true});
}
});
Why does it revert back to a regular timestamp for a time in the past upon a second implication? Furthermore, why does it jump between the seconds and the timestamp on each update?