Hi,
I am designing an application for fun.
With flutter + firebase realtime DB + cloud functions with triggers.
And one of the key "challenges" in the design is that I wanted it to be serverless.
Everything should run on firebase alone, not requiring anything else.
And I need it to be secure, since I am planning to run user code on a sandbox.
My question is on the Cloud Functions triggers.
I have a function to change usernames.
Right now it works using "query+write+delete":
export const changeUsername = functions.https.onCall((data, context) => {
if(context.auth === null)
return {'msg': 'Unauthorized user'}
const uid = context.auth!.uid;
// Check name
const newname = validateUserName(data.username)
if(newname === null)
return {'msg': 'Invalid username'}
// Check name is valid
return dbRoot.child('users/'+uid+'/info/name').once('value')
.then(function(setName: any) {
const oldname = setName.val();
if (newname === oldname) {
//console.log('Username is the same '+oldname)
return {'msg': 'Username not changed'}
}
//Check the new name is not taken
return dbRoot.child('usernames/'+newname.toLowerCase()).once('value')
.then(function(taken: any) {
if (taken.val() !== null && taken.val() !== uid) {
//console.log('Username taken by uid '+taken.val())
return {'msg': 'Username already taken'}
}
//Change it
dbRoot.child('users/'+uid+'/info/name').set(newname)
if (newname.toLowerCase() !== oldname.toLowerCase()) {
dbRoot.child('usernames/'+newname.toLowerCase()).set(uid)
dbRoot.child('usernames/'+oldname.toLowerCase()).remove()
}
//console.log('Username changed '+oldname+' => '+ newname)
return {'msg': 'Username changed to #'+newname}
});
});
});
The problem with this approach is that collisions are possible.
2 users might collide and change to the same name at the same time.
So I implemented a transaction one instead.
But I can't make it work, it times out with a recursion issue.
changeUsernameTransaction
Unhandled error RangeError: Maximum call stack size exceeded
This is the function:
export const changeUsernameTransaction = functions.https.onCall((data, context) => {
if(context.auth === null)
return {'msg': 'Unauthorized user'}
const uid = context.auth!.uid;
// Check name
const newname = validateUserName(data.username)
if(newname === null)
return {'msg': 'Invalid username'}
// Check name is valid
return dbRoot.child('users/'+uid+'/info/name').once('value')
.then(function(setName: any) {
const oldname = setName.val();
if (newname === oldname) {
//console.log('Username is the same '+oldname)
return {'msg': 'Username not changed'}
}
return dbRoot.child('usernames/'+newname.toLowerCase()).transaction(
function(currentData: any) {
if (currentData === null) {
// We can try to change it
dbRoot.child('users/'+uid+'/info/name').set(newname)
if (newname.toLowerCase() !== oldname.toLowerCase()) {
dbRoot.child('usernames/'+oldname.toLowerCase()).remove()
}
return uid;
} else {
return; // Abort the transaction.
}
}, function(error: any, committed: any, snapshot: any) {
if (error) {
console.log('Transaction failed abnormally!', error);
return {'msg': 'ERROR'};
} else if (!committed) {
return {'msg': 'Username already taken'};
} else {
return {'msg': 'Username changed to #'+newname}
}
});
});
});
I welcome:
- Any architecture change that will allow me to do this in these contention scenarios.
- How to fix the code
- How to not even need to care about these issues (by letting the DB handle it automatically)
Thanks!