Realm Flexible Sync not working properly in Swift

Hi there,

we try to implement the “Restricted News Feed” example in Swift from the Flexible Sync Permissions Guide.

We couldn’t check out the example via the template, so we had to copy the backend related things from the guide to a newly created app. (enabled email authentication, added the authentication trigger, the function to subscribe to someone else, enabled custom userdata etc…)

The backend seems to work as it should.

On client side we implemented a simple comment Object, with String Data to display:

class Comment: Object, ObjectKeyIdentifiable {
  @Persisted(primaryKey: true) public var _id: String = UUID().uuidString
  @Persisted public var ownerId: String
  @Persisted public var comment: String
}

User now can log in to the client and create comments - and sync it (works as expected). And they could subscribe to other users comments like in the example from the guide (with the same server function as in the guide). On the server we can see that the data is correct.

The problem now: on client side nothing happens when a user subscribes to another users comment. The other users comments won’t be synced…

Only when the user deletes his app from the device, reinstalls it and logs in with the same user as before - then he can see his comments and the comments from the user he subscripted to.

Here is the code for initializing the realm in SwiftUI:

let app = App(id: "xxxxxxx")
@main
struct TestSyncApp: SwiftUI.App {
  var body: some Scene {
    WindowGroup {
      if let app = app {
        AppView(app: app)
          .frame(maxWidth: .infinity, maxHeight: .infinity)
      }
      else {
        Text("No RealmApp found!")
      }
    }
  }
}

struct AppView: View {
  @ObservedObject var app: RealmSwift.App

  var body: some View {
    if let user = app.currentUser {
      let config = user.flexibleSyncConfiguration(initialSubscriptions: { subs in
        if subs.first(named: "Comment") != nil {
          return
        }
        else {
          subs.append(QuerySubscription<Comment>(name: "Comment"))
        }
      })
      OpenSyncedRealmView()
        .environment(\.realmConfiguration, config)
        .environmentObject(user)
    }
    else {
      LoginView()
    }
  }
}

struct OpenSyncedRealmView: View {
  @AutoOpen(appId: "xxxxxxx", timeout: 4000) var realmOpen

  var body: some View {
    switch realmOpen {
    case .connecting,.waitingForUser,.progress(_):
      ProgressView("waiting ...")
    case .open(let realm):
      RealmContentView()
        .environment(\.realm, realm)
    case .error(let error):
      Text("opening realm error: \(error.localizedDescription)")
    }
  }
}

And the code for displaying the comments:

struct RealmContentView: View {
  @Environment(\.realm) var realm: Realm
  @ObservedResults(Comment.self) var comments
  @State var subscribeToEmail: String = ""

  var body: some View {
    VStack {
      HStack {
        Spacer()
        Text("SubscribeTo:")
        TextField("Email", text: $subscribeToEmail)
        Button {
          if let user = app.currentUser {
            Task {
              do {
                _ = try await user.functions.subscribeToUser([AnyBSON(subscribeToEmail)])
              }
              catch {
                print("Function call failed - Error: \(error.localizedDescription)")
              }
            }
          }
        } label: {
          Image(systemName: "mail")
        }
        Text("New Comment:")
        Button {
          let dateFormatter : DateFormatter = DateFormatter()
          dateFormatter.dateFormat = "yyyy-MMM-dd HH:mm:ss.SSSS"
          let date = Date()
          let dateString = dateFormatter.string(from: date)

          let newComment = Comment()
          newComment.comment = "\(app.currentUser!.id) - \(dateString)"
          newComment.ownerId = app.currentUser!.id
          $comments.append(newComment)
        } label: {
          Image(systemName: "plus")
        }
      }
      .padding()
      if comments.isEmpty {
        Text("No Comments here!")
      }
      else {
        List {
          ForEach(comments) { comment in
            Text(comment.comment)
              .listRowBackground(comment.ownerId == app.currentUser!.id ? Color.white: Color.green)
          }
        }
        .listStyle(.automatic)
      }
    }
  }
}

Did we miss something? Do we have to manage/handle subscriptions in a different way? Or have we found a bug?

Thanks for any help!

Hey Dan - there are a couple of things you might try to resolve this. I haven’t looked in detail at this permissions model so I’m not sure which is your best fix.

One option is to try setting the rerunOnOpen parameter to true in your Sync Configuration. So:

let config = user.flexibleSyncConfiguration(initialSubscriptions: { subs in
  if subs.first(named: "Comment") != nil {
    return
  }
  else {
    subs.append(QuerySubscription<Comment>(name: "Comment"))
  }
}, rerunOnOpen: true)

This forces the subscriptions to recalculate every time the app is opened, and might resolve the need to delete/reinstall. But it would still require the user to close the app and re-open it to see the updated subscriptions. Let me know if that works, and if not, I may have some other suggestions to try.

Hey Dachary!

many thanks for the answer!
We had tried the rerunOnOpen: true before and now again on your advice.

Unfortunately that doesn’t change anything. The other users’s data remains unsynced until the user deletes and reinstalls the app.

We look forward to other suggestions!

Kind regards,
Dan

Ok, Dan - I’ve dug a little deeper here and have another suggestion to try. The docs for the restricted news feed state:

changes don’t take effect until the current session is closed and a new session is started.

I believe this is because we are effectively setting a new session role role for the user.

The Swift SDK provides APIs to suspend and resume a Sync session. I believe that if you suspend and then resume Sync, that will trigger a session role change and the user should be able to sync the new comments. This may trigger a client reset, so you’ll want to set a client reset mode in your sync configuration. This would look something like:

let config = user.flexibleSyncConfiguration(initialSubscriptions: { subs in
  if subs.first(named: "Comment") != nil {
    return
  }
  else {
    subs.append(QuerySubscription<Comment>(name: "Comment"))
  }
}, clientResetMode: .recoverUnsyncedChanges())

This should then trigger the realm to re-sync relevant comments based on the updated subscription.

We do have some work planned in the future to improve this process, but I think this is roughly what you’ll need to do to handle it currently.

Hey Dachary,

unfortunately, setting the client reset doesn’t do anything. same sync behavior as before.

we took a closer look at the realm logs: we found nothing that indicates a client reset. It seems that the client reset is never triggered and maybe that is the underlying problem?

Are you finding there is no client reset after suspending and resuming sync? I would not expect the client reset to occur until after the Sync session stops and a new one starts. This makes me wonder if there is still an active Sync session and that’s why the role change isn’t happening & new relevant docs are not getting synced.

No, we can’t find in the log anything that indicates a client reset. We call the function to subscribe to the comments of another user, then we suspend on the synced realm, then we resume the synced realm - we see in the logs that the first sync session is closed and disconnected and that another sync session is started - but nothing about a client reset.

Got it. A client reset may not be expected in this case - I know we’ve been doing work around reducing the need for client resets under certain scenarios. It’s also possible this isn’t a role change, and I’m conflating this with another permissions scenario.

I’ll tag our engineers and see if I can find any other suggestions for you.

1 Like

Are there any news regarding this issue?

We are working on a project that relies on similar functionality and this issue is currently blocking our development.
Would it be advisable to book an appointment with an engineer at MongoDB (Flex Consulting) to solve this quickly?

Thank you!

@Dan_Ivan What’s the question? I will say that we have released a Client Reset with Automatic Recovery across all of our SDKs which should perform this recovery and reset logic for you automatically under the hood -

I did check with engineering, and they spotted that the docs & backend use owner_id as the queryable field, but the snippet you’ve posted here uses ownerId. If that’s the issue, you should be seeing in the logs that the field used in permissions is not a queryable field.

If that doesn’t solve the issue, then some debugging directly with our engineers is probably the right next step.

I’m facing the same issue. And I use proper owner_id

@Alexandar_Dimcevski What error are you getting?

No error. But rerun on open doesn’t run when I close and open the app

We had a support session with a MongoDB support engineer and found out that this is not yet fully implemented in the Swift SDK: currently the realm won’t change automatically if - as in the example above - the flexible sync permissions change due to a change in the custom data (session role change).
The only safe way at the moment - according to the MongoDB engineer - is to “log out and log in the user mandatorily”. Then the data is correctly synchronized again with the new permissions.

He also told us: “It should be noted that the feature to handle role changes without client reset is under active consideration and is being developed now it may take some time to be available for the
general public.”

It would be very interesting to hear from official MongoDB staff here when we can expect this feature to be implemented - because it is not reasonable that users have to log out and log in again to get their data synced correctly!

Is there a reason to store the subscriptions inside of custom_data? Perhaps it’d work if you made a synced User object instead of using using custom_data. The RChat example does this.

edit: Also it seems the RChat app lets anyone read chats, so maybe that doesn’t actually work.

"ChatMessage": [
    {
        "name": "anyone",
        "applyWhen": {},
        "read": {},
        "write": {
            "authorID": "%%user.id"
        }
    }
],

Hi, is there any update on this issue?

@Dachary_Carey Can you help here?

Dominik,
Today, permissions are cached per sync session. as @Dan_Ivan mentioned previously. While this is an area of planned improvement, a permission change (for instance, a change to custom user data) is only guaranteed to take effect after the sync session restarts (ie disconnect and reconnect / log out and log back in).

We would recommend changing the subscription rather than the permissions to change what the user sees.

@Sudarshan_Muralidhar If I do as you say, there is absolutely Zero data security. Anyone can see anything.

That’s completely nonviable, dangerous and irresponsible to suggest!

Take the collaboration examples off the site until you actually support collaboration.

The collaboration approach suggested in the official docs does not work for reasons you wrote. Why do you suggest people do this?

-Jon