afterFind trigger for message delivery flag

Assuming there is a Message object that has an array [String] that are objectIds of everyone who has not yet read the message. ACL are set to the same array of recipients.

struct Message: ParseObject {
    
    //Those are required for Parse Object
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL? 
    
    /**
     Recipient group `objectId`
     */
    var gid: String?
    
    /**
     Array of user's `objectId` (recipients) 
     that have yet to read the message
     */
    var rids: [String]?

I am trying to implement delivery feature on messages and it is possible to remove objectId through client SDK after it fetch the Message. But this cause one extra request over the net compare to when the rids would be adjusted in afterFind as there should be “requesting user” be known.

Would it be correct to assume that afterFind trigger is a good place to remove a certain user objectId from rids and ACL? My question aims on:

  1. when exactly is the afterFind trigger called with User not being nil?
  • from my understanding this is when SDK use find or fetch query and or there is an event in live query with user logged in
  1. if there is a reasonable change that the object was also delivered to the client device after that?
  • If the afterFind trigger results in sending data to client (through SDK request of LiveQuery) I wonder how high is the change that it would really get delivered? Because once ACL is removed, the object will not be find again by given user

By looking at the source code, afterFind trigger is the last step before response. So your parse objects are fully constructed and ready to send client.

that really depends. After the afterFind trigger, parse sends response to client. But client may lost the connection. It has many factors.

Are you planing to delete messages after it get delivered to everyone?

afterFind run pretty much in the last step: parse-server/RestQuery.js at 8ed94421e69b1d4fde4ec3625a1f4cfbd6d39c2b · parse-community/parse-server · GitHub

The security rules actually run on the database, so, even if you remove the access, I believe the object will be returned. I am not sure about Live Query though, because I believe there is also a check after delivering the event to the client.

I’d write some test cases and make sure it will work for your scenario.

Planning to delete object in afterSave trigger when rids array of recipients would be empty. I am investigating to remove ACLs after each “find” to basically prevent certain client/user to fetch the same message n-times before it gets deleted (1 month old or read by all rids).

As the object should be returned on that .find() request of given user, this would be actually intended and the ACL could be saved for the case of the very next .find().

This is what I guessed also and if the ACL would be adjusted after each .find() there is a risk of some users not getting messages fetched at all. I will have to do some estimation what is better, because keeping ACL untouched would allow to re-try any fetch, but would most of the time cause unnecessary refetch.

Thank you for valuable inputs! Greatly appreciated!

Sorry for long typing, it helps me to clarify my approach and I hope it can help someone learning like me to move faster…

Investigating this further I found that most of “state of the art” apps like Signal are actually sending extra request from the client SDK to mark message as delivered and later as read. The count of write operations in the MongoDB can’t be really reduced as each “delivery” is marked by different user (i.e. user B can’t update delivery for user C and D…). What can be reduced - either for lower billing of back-end service or lower data traffic size - is the count of http requests.

I also think it is enough if:

  • Message deletion is certain (only deleted after really delivered into device)
  • “Delivered” mark is only UI info feature and can be considered unreliable

Therefore I think there are two ways how to approach this:

Split “delivery” from "fetch"

  • When fetched into the device, the recipient objectId would be removed from ACL and rids with a call by SDK (.save(), cloud code function,…) → this prevents refetching given message again (ACL) and marks as delivered (rids) and can happen in batch
  • In beforeSave trigger of Message object would check when the ACL contains only the sender’s objectId or nothing → then the object gets deleted
  • In case the chat is currently on the screen and LiveQuery active, the afterFind trigger would be the place where to remove recipient from rids → this keeps the delivery unreliable but immediate and without http request.

This might produce smooth “User B” experience, because he sees messages delivery mark immediately, but also cause higher count of database writes (one for .rids and one for ACL) . I am also not sure if it is so easy to loop through ACLs in the cloud code and might not be possible for everyone (i.e. when using Role).

Set “delivery” in batches

  • use Cloud function sending array of Message.objectId to set rids and ACL for all messages fetched on app start → 1 request, nothing delayed from users perspective, object fetched by recipient only once.
  • delete Message when rids is empty in beforeSave trigger
  • for the case of currently running chat on screen set rids only for that particular chat either every time new message comes or batch the response based on some criteria like:
    • user A receives n messages and only sends out “delivery” response when sending his message back to user B (here can be even a context in .save() used instead of cloud function)
    • user A sends out “delivery” response when he leaves the chat view or close app
  • for all other conversations the delivery would be again in batch with a cloud function (i.e. when user enters conversation list view, app resigns active,…)

This is a trade-off of request count and delivery mark being immediate.

A sender either has always his ACL on a Message sent by him and will fetch the message until the message gets deleted (can see delivery updates) or does not set his ACL when saving new message (he does not care about delivery status) and never re-fetch his messages.

I think the second solution is more scalable and can be further tuned. Or do I miss any simple logic here?

If anyone would share ho she/he handles deletion of delivered messages I would appreciate it! Thank you!

You wanna reduce api request counts am I right? In your chat app I believe you are using push notifications too. Push notification uses some kind of service(Thats what its called in android). It listens notification in the background. You can store the recieved messages object ids. And send some request to your cloud code with the array of ids. Then you can mark all of them as delivered. Or you can do it on apps startup. But this doesnt work if user is on the chat screen and recieves messages realtime.

1 Like