still can't figure out how to order async tasks for user deletion

155 views Asked by At

So I'm trying to make sure a set of async tasks get executed in a specific order when a user is being deleted.

So what I want to happen is :

  • Check if user has added guests with their purchase
  • if user has no guests or any purchases at all, return from the function and continue with deletion process (bullet point 6)
  • if user has guests for any of their purchases, delete every single guest
  • once all the guests are deleted, go ahead and delete every purchase they made
  • once all purchases made are deleted, go ahead and delete the actual user itself out of Firestore
  • 2 seconds after that, I delete the user out of firebase auth just to make sure there are no crashes trying to delete documents with an empty user
  • then I simply segue to the main menu

So I'm trying to accomplish this with this block of code in my function:

let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
            let semaphore = DispatchSemaphore(value: 0)
            
            
            self.deleteButton.isHidden = true
            self.loadingToDelete.alpha = 1
            self.loadingToDelete.startAnimating()
            
            DispatchQueue.global(qos: .background).async {
                self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
                    guard error == nil else {
                        print("The docs couldn't be retrieved for deletion.")
                        return
                    }
                    
                    guard querySnapshot?.isEmpty == false else {
                        print("The user being deleted has no events purchased.")
                        return
                    }
                    
                    for document in querySnapshot!.documents {
                        let docID = document.documentID
                        
                        self.db.collection("student_users/\(user.uid)/events_bought/\(docID)/guests").getDocuments { (querySnap, error) in
                            guard querySnap?.isEmpty == false else {
                                print("The user being deleted has no guests with his purchases.")
                                return
                            }
                            
                            for doc in querySnap!.documents {
                                let guest = doc.documentID
                                self.db.document("student_users/\(user.uid)/events_bought/\(docID)/guests/\(guest)").delete { (error) in
                                    guard error == nil else {
                                        print("Error deleting guests while deleting user.")
                                        return
                                    }
                                    print("Guests deleted while deleting user!")
                                    semaphore.signal()
                                }
                                semaphore.wait()
                            }
                        }
                    }
                }
                
                
                self.db.collection("student_users/\(user.uid)/events_bought").getDocuments { (querySnapshot, error) in
                    guard error == nil else {
                        print("There was an error retrieving docs for user deletion.")
                        return
                    }
                    guard querySnapshot?.isEmpty == false else {
                        return
                    }
                    for document in querySnapshot!.documents {
                        let docID = document.documentID
                        
                        self.db.document("student_users/\(user.uid)/events_bought/\(docID)").delete { (err) in
                            guard err == nil else {
                                print("There was an error deleting the the purchased events for the user being deleted.")
                                return
                            }
                            print("Purchases have been deleted for deleted user!")
                            semaphore.signal()
                        }
                        semaphore.wait()
                    }
                    
                }
        
                
                self.db.document("student_users/\(user.uid)").delete(completion: { (error) in
                    
                    guard error == nil else {
                        print("There was an error deleting the user document.")
                        return
                    }
                    print("User doc deleted!")
                    semaphore.signal()
                })
                semaphore.wait()
                
                
            }
            
            DispatchQueue.main.asyncAfter(deadline: .now()+1.5) {
                user.delete(completion: { (error) in
                    guard error == nil else {
                        print("There was an error deleting user from the system.")
                        return
                    }
                    print("User Deleted.")
                })
               
                
            }
        
            self.loadingToDelete.stopAnimating()
            self.performSegue(withIdentifier: Constants.Segues.studentUserDeletedAccount, sender: self)
            
            
        }

I've been trying to play around with DispatchSemaphore() for the last couple of hours and implement it into my code, but it just doesn't do what I expect it to do. I would read up on articles and examples of DispatchSemaphore() online but the scenarios aren't exactly the same as mine in regards to what I specifically want to do.

When this alert action gets triggered on tap, nothing prints, and it just ends up spinning forever, the user isn't deleted out of Firebase Auth and there is still leftover data in the Firestore database like so:

error

I just basically want to figure out the best way to order these async tasks in the ordered list above and have a clean user deletion with no leftover in the database. Thanks in advance.

1

There are 1 answers

4
DIGI Byte On

You should be handling this with a Firebase Cloud Function which has multiple ways of reacting to client requests and database changes. This does require billing and migrating your code to javascript with node v10 which is fairly straightforward due to the consistent methods of firebase across most languages.

Firebase Function

Two popular methods are simply importing the firebase cloud functions module into your app or calling the request over https, each has its own entry points with pros and cons which are worth reading into.

From there, you would delete the core files that would impact the user immediately, then updating the client on its result before proceeding with your clean-up of residual files.

Firestore Trigger

An alternative that is just as sound and more manageable from potential abuse is invoking a trigger based on a document deletion, you can use this to then trigger other documents to proceed to be removed and cleaned up

You can read about them below, and it can contain fundamentally the same logic in both situations but this option doesn't require the bulky firebase functions module.

https://firebase.google.com/docs/functions/firestore-events#function_triggers

Update: Async

Async methods are simply functions that are flagged as async that allow tasks to operate without blocking structure, this allows multiple tasks to be fired without depending on another. However, to pause your code to wait for something to be done, you can append the await flag

function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling');
  const result = await resolveAfter2Seconds();
  console.log(result);
  // expected output: "resolved"
}

asyncCall();

reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

Update: Promises

Promises work the same as async functions and run independently to the parent function and itself can be flagged with await if need be.

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('foo');
  }, 300);
});

myPromise
  .then(handleResolvedA, handleRejectedA)
  .then(handleResolvedB, handleRejectedB)
  .then(handleResolvedC, handleRejectedC);

reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise