Firebase Firestore: when do promises from offline write operations resolve? - javascript

I activated offline as stated in the docs like:
firebase
.firestore()
.enablePersistence()
.then(() => {
console.log('offlinemode acctivated')
})
The log appears as I would expect.
When adding data like so:
db
.collection('foo')
.add({foo: 'bar'})
.then(docRef => {
console.log('Added Foo: ', docRef.id)
// do some stuff here with the newly created foo and it's id.
})
.catch(console.error)
Neither .then() nor .catch() are getting called while offline. This is even though the object is added to the foo collection in my offline DB as this callback is executed:
db
.collection('foo')
.onSnapshot(callback)
Am I missing something? I would expect the promise to either fail or resolve, so I can react accordingly.

Promises from write operations in Firestore will only resolve when there is confirmation from the server that the write completed, even though they may successfully be written to local cache.

Here's my solution:
I wrap the call in a function that should eventually return a resolving promise no matter the offline/online status
I then get the saved doc from onSnapshot which returns the doc written to local cache (works both online and offline).
Here's my code (with a little typescript):
export function dbWritePromise(functionPromise: Promise<any>): Promise<any>{
if(window.navigator.onLine){
return functionPromise
}
else{
return Promise.resolve()
}
}
// I grabbed this function from a Github issue one upon a time
export function docSnapshotPromise(ref: firebase.firestore.DocumentReference): Promise<any>{
return new Promise((resolve, reject) => {
const unsubscribe = ref.onSnapshot(doc => {
resolve(doc)
unsubscribe()
}, err => {
reject(err)
unsubscribe()
})
})
}
In use (I'm using the update function here, but add would work the same way) This code is working with documents from a collection called organizations:
try{
//update org doc
await dbWritePromise(orgRef.update({
name: 'New and improved name here'
}))
// wait for this updated doc to be written to local cache, then we can get the updated org
const updatedOrgRef = await docSnapshotPromise(orgRef)
const updatedOrg = updatedOrgRef.data()
console.log(updatedOrg.name) // outputs the new and improved name
}
catch (err) { handleError(err) }
The error thrown might be some error with local cache, or it might be a server error such as a permissions error returned by the Firestore rules (when online). Obviously any error from the server during offline mode would silently fail, even when the app is back online.
I'd love to see other peoples' solutions here!

Related

mssql execute: return both promise and recordsets

I am using node-mssql to perform a bunch of operations inside a transaction. Spcifically, the methods used in order are begin tran,bulk,bulk,exec,commit. Here is part of the code, which works as-is:
// bulk insert #columns
.then(result => {
const request=new sql.Request(globalTx);
const tableObj = new sql.Table('#columns'); //#columns
/* some code to build the desired tableObj*/
return request.bulk(tableObj);
})
// exec proc
.then(result => {
returnObject.lastSuccessfulStep='bulk #data'
returnObject["#columns rows"]=result.rowsAffected
const request=new sql.Request(globalTx);
/* some code to build the desired proc call*/
return request.execute('spc_sqlDMLCommand');
})
//commit
.then(result => {
return globalTx.commit(); // returns promise
})
So you can see the use of chained .thens. There is a catch() at the end.
My problem is that in the case od the execute method, while the then() /catch() promise structure feels friendly to me, I also need the information that the method returns through its callback. Citing the reference about the second parameter:
callback(err, recordsets, returnValue) - A callback which is called
after execution has completed, or an error has occurred. returnValue
is also accessible as property of recordsets. Optional. If omitted,
returns Promise.
Is there a way to save err/recordsets/returnValue to some objects while still maintaining the flow, ie not having to cut-paste the subsequent then()s into the execute's callback?
Solution attempt:
I tried using a callback:
request.execute('spc_sqlDMLCommand',function cb(a,b,c){
console.log('a:\n',a,'b:\n',b,'c:\n',c)
});
but I got this error:
message: 'Requests can only be made in the LoggedIn state, not the
SentClientRequest state', code: 'EINVALIDSTATE'
Following this up on the internet, it seems to be about multiple queries having problems. But, it already works for me if I do it in the way mentioned above...

Returning null in firestore function trigger after async code?

I'm having trouble understanding how to properly end a firestore trigger. From what I read from this and this, it seems you should only return null to end a function if there's no async code such as to quickly end if a condition isn't met. When I return null in my scenario below, it seems to work fine. Is there a better practice for what I'm doing that I'm missing?
I need to log my own custom error message which why I need the catch. I know I could return Promise.all here instead of null in the try block, but this is just sudo code for my scenario.
export const delacc = functions.auth.user().onDelete(async (user) => {
const userUID = user.uid;
try{
await admin.firestore().collection("users").doc(userUID).delete();
await admin.firestore().collection("spam").doc(userUID).delete();
await admin.firestore().collection("photos").doc(userUID).delete();
return null;
}catch(error){
functions.logger.error(error)
return null
}
});
There's no hard requirement to return null. In fact, async functions always return a promise, no matter what you do inside the function. The promise it returns is based on the completion of any other promises that you await during processing. Even if you explicitly return null, the function is still actually just returning a promise that is immediately fulfilled with the value null. It has no effect on the final outcome, since Cloud Functions onDelete triggers don't use the fulfilled value in any way. The important thing is that the function indeed returns a promise that indicates when all the work is complete (and, as I stated, async functions always do that if you use await correctly inside the function on all async work).
When you're not using async/await, I advise programmers to always return a promise, or null if there is no async work. The null there is an explicit way to tell the reader of your code that you do not intend for Cloud Functions to wait for any async work before fully terminating the function. It helps also helps satisfy eslint or TypeScript warnings, which will suggest to you that you should return something. The value itself isn't really important - it's what you're communicating to others about the termination of your function. Code readability is important if you work with others.
What I'd do is make the delete operations atomic—delete all of the documents or none of them—and return the promise returned by the batch since there isn't any other task performed in this function (which makes me think returning null isn't a necessary abstraction). If the batch throws an error, throw the client a new HTTPS error (along with the batch error) which (a) automatically terminates the cloud function and (b) gives the client the batch error to evaluate, which, depending on the reason, could warrant a retry or an error message to the end user.
export const delacc = functions.auth.user().onDelete(async (user) => {
const userUID = user.uid;
const db = admin.firestore();
const batch = db.batch();
try {
batch.delete(db.collection("users").doc(userUID));
batch.delete(db.collection("spam").doc(userUID));
batch.delete(db.collection("photos").doc(userUID));
const writeResult = await batch.commit();
return writeResult;
} catch(error) {
functions.logger.error(error);
throw new functions.https.HttpsError("unknown", "batch-error", error);
}
});

Getting Internal Error when calling Firebase Cloud Function Directly

I'm getting the following Error when trying to call my Firebase Cloud Function directly:
Error: internal
at new HttpsErrorImpl (error.ts:65)
at _errorForResponse (error.ts:175)
at Service.<anonymous> (service.ts:276)
at step (tslib.es6.js:102)
at Object.next (tslib.es6.js:83)
at fulfilled (tslib.es6.js:73)
I've noticed that users have seen similar issues in the past where the Firebase version 7.22.0 was causing it, and it was resolved in 7.22.1 but I'm on 8.3.0, so that shouldn't be a problem.
My cloud function is never being triggered, I don't see any errors in the Firebase Functions log.
Here is my client-side function:
async function testCallFunction() {
const callTest = functions.httpsCallable('callTest');
return await callTest({ companyId: coRef })
.then((result) => {
console.log(result)
}).catch(err => {
console.log(err)
})
}
And here is my Cloud Function:
exports.callTest = functions.https.onCall((data, context) => {
console.log('Call Test Fired')
return admin.firestore().collection('users').doc(context.auth.uid).get()
.then((doc) => { return doc.data()})
.catch(err => {throw new functions.https.HttpsError(err.message)})
})
But it's never reaching the cloud function I get the internal error only and no logs in the cloud function logs.
I'm not sure what I'm doing wrong? I have made sure I'm using the latest firebase JS SDK (8.3.0) and I've tried to stick as close to the doc as I can. Is there anything obvious I'm doing wrong here?
Thanks
I'm seeing the exact same symptoms while using AngularFireFunctions httpsCallable method. Recently updated firebase sdk to 8.3.1 from 8.2.4 and I suspect this introduced the https callable internal error. Downgrading to 8.2.4 resolves the issue:
npm install firebase#8.2.4 --save

Is there a more concise way to resolve or reject Promises that are created in a forEach loop?

I want to read data from Firebase Real-Time Database and Firebase Storage. Reading RTDB happens first. This is because the name of the files that are stored in Firebase Storage are the keys of the data in the RDTB. I have the following code:
let newReadRef = firebase.database().ref('someRef').limitToLast(3).once('value').then(snapshot => {
let promiseArray = [];
snapshot.forEach(e => {
promiseArray.push(new Promise((resolve, reject) => {
//Do all the things I need to do before trying to get data from my Firebase Storage
let filePathRef = firebase.storage().ref(e.val().key);
filePathRef.getDownloadURL().then(url => {
//Do something with the url
resolve('Promise resolved');
}).catch(e => {
console.log(e);
reject('Rejected');
});
}));
});
Promise.all(promiseArray).then(someFunction);
});
The idea is:
Retrieve some data from my RTDB, its key will be used later to retrieve data in my Firebase Storage
Create a new Promise for each data read in my RTDB
For each Promise created, read data from Storage using key obtained from RTDB data. Resolve the Promise when data is read successfully. Otherwise, reject.
After all Promises have been created, when all Promise resolves, do something using someFunction that will alter the data created
It seems like my code is a little too nested. Is there a better way to achieve the above?
No, the code is about as flat as you can get it, though, since the last thing you're doing returns a Promise, you won't need to create a Promise
snapshot.forEach(e => {
//Do all the things I need to do before trying to get data from my Firebase Storage
let filePathRef = firebase.storage().ref(e.val().key);
promiseArray.push(
filePathRef.getDownloadURL()
.then(url => {
//Do something with the url
return 'Promise resolved';
}).catch(e => {
console.log(e);
throw 'Rejected';
});
);
});
I was going to suggest using .map instead of .forEach, but snapshot isn't a Javascript array if I recall (it has a forEach, but no map method)

Vuex (or maybe just JS) chained promises in Actions and context/commit

We're having a hard time figuring out how context (or specifically { commit } is handled in chained promises with transpiled ES6 code. Below is one example of a Login action that authenticates and then subscribes using RxJS to the user as a stream. We need to commit several mutations throughout the process, but keep getting commit is not a function errors.
Does anyone know of or have an example of something like this or can anyone provide any basic guidelines on where and how context/commit are handled in this scenario - e.g. when can ES6 be used vs not, and/or where is context hoisted or not (if at all), and or is there a simpler approach to all this like maybe wrapping things all in a master promise? Since we need to potentially commit at each step in the promise chain, we cannot see how some of this could work:
const actions = {
login ({commit}, creds) { // need to commit here
commit('toggleLoading')
api.authenticate({
strategy: 'local',
...creds
})
.then(function (result) {
return api.passport.verifyJWT(result.accessToken)
})
.then(function ({commit}, payload) { //need to commit here
console.log(commit)
return api.service('users').get(payload.userId)
.subscribe(commit('setUser', user)) // need to commit here - but commit is not a function error
})
.catch(function ({commit}, error) {
commit('setErr', `ERROR AUTHENTICATING: {$err.message}`) // need to commit here but commit is not a function error
commit('toggleLoading')
})
}
All of the examples we find are way simplistic and only show one commit per action (or maybe 2 wrapped in an if). Any help or feedback is appreciated!
Firstly, the callback functions in .then and .catch take a single argument, you've coded two ... however, commit from the login arguments is still in scope, so it's quite simple to fix
Your code can be simplified as follows
const actions = {
login ({commit}, creds) {
commit('toggleLoading');
api.authenticate({strategy: 'local', ...creds})
.then(result => api.passport.verifyJWT(result.accessToken))
.then(payload => api.service('users').get(payload.userId).subscribe(commit('setUser', user)))
.catch(function (error) {
commit('setErr', `ERROR AUTHENTICATING: ${error.message}`);
commit('toggleLoading');
});
}
Note: you have {$err.message} in the .catch, whereas I beleive that should be ${error.message}

Categories

Resources