Computed property in realm. How to update a different realm from a realm collection listener?

602 Views Asked by At

I'm building a chat app. Each channel has many messages. I am building the channel list view where I want to display all the channels sorted by the most recent message sent at per channel.

Each time a message is sent or received I would like to keep the channel.latestMessageUpdatedAt up to date so that I can sort channels later.

I would like to separate concerns and not have to remember to update channels each time messages are updated.

My strategy is to update the channel inside the listener to the message realm, but I get the following error

Error: Wrong transactional state (no active transaction, wrong type of transaction, or transaction already in progress)
const ChannelSchema = {
  name: "channel",
  primaryKey: "id",
  properties: {
    id: "string",
    latestMessageUpdatedAt: "date",
  },
};

const MessageSchema = {
  name: "message",
  primaryKey: "id",
  properties: {
    id: "string",
    channelId: "string",
    text: "string",
    updatedAt: "date",
  },
};

const realm = await Realm.open({
  path: "default.realm",
  schema: [ChannelSchema, MessageSchema],
  schemaVersion: 0,
});

const messagesRealm = realm.objects("message");
messagesRealm.addListener((messages, changes) => {
  for (const index of changes.insertions) {
    const channel = realm.objectForPrimaryKey(
      "channel",
      messages[index].channelId
    );
    if (!channel) continue;

    const message = messages[index];
    channel.latestMessageUpdatedAt = new Date();
  }
});

I've checked the docs there seems to be no reason why this wouldn't be possible.

Perhaps there is a better way of having this computed field.

Note I thought about having embedded objects / a list of messages on the channel but the number of messages could be up to 10k, I don't want that all returned at once into memory.

I've also tried doing

realm.write(() => {
    channel.latestMessageUpdatedAt = new Date();
});

but I get the error that a transaction is already in progress.

1

There are 1 best solutions below

13
Jay On

The OP requested a Swift solution so I have two: which one is used depends on the data set and coding preference for relationships. There is no automatic inverse relationship in the question but wanted to include that just in case.

1 - Without LinkingObjects: a manual inverse relationship

Let's set up the models with a 1-Many relationship from a channel to messages and then a single inverse relationship from a message back to it's parent channel

class ChannelClass: Object {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var channelName = ""
    @Persisted var messageList: List<MessageClass>
}

class MessageClass: Object {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var msg = ""
    @Persisted var msgDate = ""
    @Persisted var myChannel: ChannelClass!
}

Then after we populate Realm we have some objects that look like this - keeping in mind that different channels will have had messages added at different times

channel 0
   message 1
   message 3
   message 4

channel 1
   message 5
   message 9
   message 10

channel 2
   message 2
   message 6
   message 7
   message 8

It would look like this because: suppose a user posts a message to channel 0, which would be message 1. Then a day later another user posts a message to to channel 2, which would be message 2. Then, on another day, a user posts a message to channel 0 which would be message 3. Etc etc

Keeping in mind that while Realm objects are unsorted, List objects always maintain their order. So the last element in each channels list is the most current message in that channel.

From there getting the channels sorted by their most recent message is a one liner

let messages = realm.objects(MessageClass.self).sorted(byKeyPath: "msgDate").distinct(by: ["myChannel._id"])

If you now iterate over messages to print the channels, here's the output. Note: This is ONLY for showing the data has already been retrieved from Realm and would not be needed in an app.

Channel 0 //the first message
Channel 2 //the second message
Channel 1 //the third message? Not quite

Then you say to yourself "self, wait a sec - the third message was in channel 0! So why output channel 1 as the last item?"

The reasoning is that the OP has a requirement that channels should only be listed once - therefore since channel 0 was already listed, the remaining channel is channel 1.

2 - With LinkingObjects: Automatic inverse relationship

Take a scenario where LinkingObjects are used to automatically create the backlink from the messages object back to the channel e.g. reverse transversing the object graph

class MessageClass: Object {
    @Persisted(primaryKey: true) var _id: ObjectId
    
    @Persisted var msg = ""
    @Persisted var msgDate = ""
    @Persisted(originProperty: "messageList") var linkedChannels: LinkingObjects<ChannelClass>
}

the thought process is similar but we have to lean on Swift a little to provide a sort. Here's the one liner

let channels = realm.objects(ChannelClass.self).sorted { $0.messageList.last!.msgDate < $1.messageList.last!.msgDate }

What were doing here is querying the channels and using the msgDate property from the last message object in each channels list to sort the channels. and the output is the same

Channel 0 //the first message
Channel 2 //the second message
Channel 1 //see above

The only downside here is this solution will have larger memory impact but adds the convenience of automatic reverse relationships through LinkingObjects

3 Another option

Another options to add a small function and a property to the Channel class that both adds a message to the channels messagesList and also populates the 'lastMsgDate' property of the channel. Then sorting the channels is a snap. So it would look like this

class ChannelClass: Object {
    @Persisted(primaryKey: true) var _id: ObjectId
    
    @Persisted var channelName = ""
    @Persisted var messageList: List<MessageClass>
    @Persisted var lastMsgDate: String
    
    func addMessage(msg: MessageClass) {
        self.messageList.append(msg)
        self.lastMsgDate = msg.msgDate
    }
}

When ever a message is added to the channel, the last message date is updated. Then sort channels by lastMsgDate

someChannel.addMessage(msg: someMessage)

Note: I used Strings for message dates for simplicity. If you want to do that ensure it's a yyyymmddhhmmss format, or just use an actual Date property.