1

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.

dante
  • 105
  • 11
  • As a side note, I think the cleanest way to delete the user would be to delete as little as necessary through the client, which would be the user object in Firebase Auth. If the user wants out let then let them out. When this object is successfully deleted, take the user to the appropriate place and as far as they are concerned the task is complete. From there, I would call a cloud function that goes through the database and deletes all of the user's remnants. This function would be recursive in case there are large batches of documents to delete and errors surface. – trndjc Apr 25 '21 at 23:55
  • I see where you're coming from with that but it would basically mean I would have to do everything that i'm doing on the client side onto the server side which is basically repetitive. I know you're probably thinking I have one type of user and can use `auth().onDelete()` in CF, but I have two types of users with different queries in the deletion process. Either way, I've figured out a way that deletes everything correctly in order, the only issue now is that when everything is done being deleted and the view segues, nothing can be tapped on the screen essentially making it frozen. @bxod – dante Apr 26 '21 at 00:48

1 Answers1

1

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

DIGI Byte
  • 4,225
  • 1
  • 12
  • 20
  • I'm familiar with both and currently have them implemented in my app, but how would that solve the issue? I would still be figuring out how to order the tasks asynchronously on the server side, would I not? The main issue is order of tasks, as long as they can execute in order with no residual data left over, that's all that matters to me. I currently have the desired functionality that I wanted, the only issue is that I can't interact with the UI after deletion. @DIGI Byte – dante Apr 26 '21 at 00:55
  • Asynchronously simply means to allow multiple tasks to be processed at the same time, which can by done by simply wrapping it inside an async block. if you need any tasks to wait, you then flag it with `await` and utilize promise chains. – DIGI Byte Apr 26 '21 at 01:07
  • Yeah I'm gonna try seeing if I can figure it out client side first, I know it can be done and I'm pretty much closer to a solution on that end, I genuinely have no idea how to implement what you posted and convert it into my deletion process. If all else fails, I'll give what you posted a try but for now, sticking with client side for this one. @DIGI Byte – dante Apr 26 '21 at 01:26
  • if you are interested, I run a Firebase Discord server at https://discord.firebase.me/ we can help dive and debug your questions since stackoverflow is normally against that – DIGI Byte Apr 26 '21 at 01:37