Search code examples
iosswiftswiftuiaws-amplifycombine

AWS Amplify - sync DataStore


I am trying to build a prototype app to evaluate the usage of AWS-Amplify (DataStore) in our next App. I am facing an issue when trying to sync 2 Clients. I set up my AWS-DataStore as explained by the tutorial here: https://docs.amplify.aws/lib/datastore/getting-started/q/platform/ios by using Cocoapods. Regarding Cocoapods everything is working as expected. I tried some testing which can also be seen under the link above with "manipulating data". I also did the "syncing data to cloud" section. I can see my data in dynamoDB in the AWS console. When I am adding an entry I can see it in the console everything is working as expected but i have a problem in two scenarios:

Let's say i have 2 clients A and B (both running iOS 13 - it doesn't matter here if it is a real device or on simulator) and I want to these clients to stay in sync. For this purpose I added a PostStore (I am using the example schema from the AWS-link above atm) which looks like this:

import Foundation
import Combine
import SwiftUI
import Amplify

class PostStore: ObservableObject {
    
    @Published private(set) var posts: [Post] = []
    
    var postSubscription: AnyCancellable?
    
    init() {
        self.getAllPostsFromDataStore()
    }
    
    deinit {
        self.unsubscribeFromDataStore()
    }
    
    func getAllPostsFromDataStore() {
        
        Amplify.DataStore.query(Post.self) { (result) in
            switch result {
            case .success(let posts):
                DispatchQueue.main.async {
                    print("Got \(posts.count) Posts from DataStore initially")
                    self.posts = posts
                }
            case .failure(let error):
                print("Error getting Posts from DataStore: \(error.localizedDescription)")
            }
        }
    }
    
    func addRandomPostToDataStore() {
        
        let post = Post.getRandomPost()
        self.addPostToArray(post)
        
        print("Fire: \(post.id)")
        
        Amplify.DataStore.save(post) {
            
            switch $0 {
            case .success(let post):
                print("Added post with id: \(post.id)")
            case .failure(let error):
                print("Error adding post with title: \(post.title) Error: \(error.localizedDescription)")
            }
        }
    }
    
    func deletePostFromDataStore(for indexSet: IndexSet) {
        
        let postsToDelete = indexSet.map { self.posts[$0] }
        
        self.posts.remove(atOffsets: indexSet)
        
        for post in postsToDelete {
            
            Amplify.DataStore.delete(post) { (result) in
                switch result {
                case .success():
                    print("Deleted Post from DataStore")
                case .failure(let error):
                    print("Error deleting Post From DataStore: \(error)")
                }
            }
        }
    }
        
    func subscribeToDataStore() {
        
        postSubscription = Amplify.DataStore.publisher(for: Post.self)
            .sink(receiveCompletion: { (completion) in

                print("Completion!")

                if case .failure(let error) = completion {
                    print("Subscription received Error: \(error.localizedDescription)")
                }

            }, receiveValue: { (changes) in
                
                
                //print("Subscription received mutation: \(changes)")
                print("\n\n\n")
                print("\(try! changes.toJSON())")
                print("\n\n\n")
//                print("Changes!")
                let newPost = try! changes.decodeModel(as: Post.self)

                DispatchQueue.main.async {

                    switch changes.mutationType {
                    case "create":
//                        print("Create Subscription")
                        self.addPostToArray(newPost)
                        break
                    case "update":
                        print("Update Subscription")
                        self.updatePostInArray(newPost)
                        break
                    case "delete":
                        print("Delete Subscription")
                        self.deletePostFromArray(newPost)
                        break
                    default:
                        print("AnotherType?")
                        print(changes.mutationType)
                        break
                    }
                }
                print("\n")
            })
    }
    
    func unsubscribeFromDataStore() {
        postSubscription?.cancel()
    }
    
    private func addPostToArray(_ post: Post) {
        
        if !self.posts.contains(post) {
            self.posts.append(post)
        }
    }
    
    private func deletePostFromArray(_ post: Post) {
        
        if let index = self.posts.firstIndex(of: post) {
            self.posts.remove(at: index)
        }
    }
    
    private func updatePostInArray(_ post: Post) {
        print("update?")
    }
}

The subscribeToDataStore method is triggered via the SwiftUI-View (in onAppear) and is the key thing in here. If I am adding some Posts with Client A (slowly) then Client B receives every change and is adding it to the store (I visualized this with a SwiftUI View but that doesn't matter in this example). No Problems here everything is working as expected.

But

  1. When I am adding new entries (Posts) very fast with Client A then some of the new entries are not getting fetched by B.
  2. When I am adding more than 1 entry while Client B is offline (in Flight Mode) and I turn Flight Mode off only the latest entry is fetched per subscription.

In both cases when I trigger getAllPostsFromDataStore every single entry is fetched and we are good to go again.

So my question is: Why is the subscription not fetching every single entry?

There is an interesting fact when doing scenario 1: A common log message when receiving an entry per subscription looks like this:

WebsocketDidReceiveMessage - {MyObject}

Then I am logging the object in the sink 'receiveValue' completion again:

{MyObject}

But when adding a few entries fast (in a short time) the log message from Amplify sometimes looks like that: (And this is where the first "Object" is missing in the subscription fetch):

WebsocketDidReceiveMessage - {MyObject}
WebsocketDidReceiveMessage - {MyObject2}

Then I am logging the object in the sink 'receiveValue' completion again and there is only the last object getting logged:

{MyObject2}

Seems like that {MyObject} got cancelled or something when adding more than one entries fast. Like mentioned above, when B is in Flight Mode or has no connection only the last added entry is fetched. (But I also receive only the last log message for WebsocketDidReceiveMessage).

Am I missing a thing here or is this a bug in the Amplify-DataStore SDK?

Excuse my bad english please. I am not a native speaker and sometimes it is hard to tell exactly what is the problem. If you have any questions regarding this problem or need more information just comment and I will edit my question providing everything you need.

Best Regards


Solution

  • For anyone who also came across this problem:

    I filed an Issue here and the problem seems to be fixed in version 1.3.1 with PR #756

    Have fun!