From c7433a7778ed40272e24e9710e4af8b44ac07f18 Mon Sep 17 00:00:00 2001 From: Diana Perez Afanador Date: Mon, 23 Jan 2023 11:25:45 +0100 Subject: [PATCH] Redesign `@AsyncOpen` and `@AutoOpen` initialisers to allow setting a client reset mode for any sync configuration, and avoid confusion with the configuration and the sync configuration from the user on each intialiser. --- CHANGELOG.md | 21 +- .../SwiftUIServerTests.swift | 365 +++++++++++++++--- .../SwiftUISyncTestHost/ContentView.swift | 37 +- .../SwiftUISyncTestHostUITests.swift | 57 +++ RealmSwift/SwiftUI.swift | 110 ++++-- 5 files changed, 493 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdf78fc812..1379a5b6b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,30 @@ x.y.z Release notes (yyyy-MM-dd) ============================================================= ### Enhancements -* None. +* Redesign `@AsyncOpen` and `@AutoOpen` initialisers to allow setting a client reset modes for any sync configuration, + and avoid confusion with the injected configuration and the sync configuration from the user on each intialiser. ### Fixed * ([#????](https://github.com/realm/realm-swift/issues/????), since v?.?.?) * None. - +### Breaking Changes +* `AsyncOpen.init(appId:partitionValue:configuration:timeout)` and + `AutoOpen.init(appId:partitionValue:configuration:timeout)` will change behavior. Previously + injecting both the configuration and a partition value will make the property wrappers to set + the injected configuration (if not nil) combine with the syncConfiguration from the + `user.configuration(partitionValue:)`. This will change to either user the injected configuration (if not nil) + or the configuration obtained from `user.configuration(partitionValue:)` for the current logged user. + This API will be deprecated on the next major release. +* `AsyncOpen.init(appId:configuration:timeout)` and `AutoOpen.init(appId:configuration:timeout)` + will change behavior. Previously this initialiser will set the injected configuration (if present) + combine with the flexible sync configuration `user.flexibleSyncConfiguration` from the logged user. + This will change to use the injected configuration which should already include a sync configuration. +* `AsyncOpen.init(appId:partitionValue:timeout)` and `AutoOpen.init(appId:partitionValue:timeout)` + are two new initialisers which allow us to configure the property wrappers for a partition based sync + configuration for the current logged user. +* `AsyncOpen.init(appId:timeout)` and `AutoOpen.init(appId:timeout)` are two new initialisers which + allow us to configure the property wrappers for a flexible sync configuration for the current logged user. ### Compatibility * Realm Studio: 11.0.0 - 12.0.0. diff --git a/Realm/ObjectServerTests/SwiftUIServerTests.swift b/Realm/ObjectServerTests/SwiftUIServerTests.swift index 660a9bbb92..c078f2eeea 100644 --- a/Realm/ObjectServerTests/SwiftUIServerTests.swift +++ b/Realm/ObjectServerTests/SwiftUIServerTests.swift @@ -30,6 +30,12 @@ import RealmSyncTestSupport @MainActor class SwiftUIServerTests: SwiftSyncTestCase { + enum OpenType { + case configuration(Realm.Configuration) + case pbs(String) + case flexibleSync + } + // Configuration for tests private func configuration(user: User, partition: T) -> Realm.Configuration { var userConfiguration = user.configuration(partitionValue: partition) @@ -47,13 +53,23 @@ class SwiftUIServerTests: SwiftSyncTestCase { var cancellables: Set = [] // MARK: - AsyncOpen - func asyncOpen(user: User, appId: String? = nil, partitionValue: T, timeout: UInt? = nil, - handler: @escaping (AsyncOpenState) -> Void) { - let configuration = self.configuration(user: user, partition: partitionValue) - let asyncOpen = AsyncOpen(appId: appId, - partitionValue: partitionValue, + func asyncOpen(appId: String? = nil, openType: OpenType, timeout: UInt? = nil, + handler: @escaping (AsyncOpenState) -> Void) { + let asyncOpen: AsyncOpen + switch openType { + case .configuration(let configuration): + asyncOpen = AsyncOpen(appId: appId, configuration: configuration, timeout: timeout) + case .pbs(let partitionValue): + asyncOpen = AsyncOpen(appId: appId, + partitionValue: partitionValue, + timeout: timeout) + case .flexibleSync: + asyncOpen = AsyncOpen(appId: appId, + timeout: timeout) + } + _ = asyncOpen.wrappedValue // Retrieving the wrappedValue to simulate a SwiftUI environment where this is called when initialising the view. asyncOpen.projectedValue .sink(receiveValue: handler) @@ -63,10 +79,10 @@ class SwiftUIServerTests: SwiftSyncTestCase { } func testAsyncOpenOpenRealm() throws { - let user = try logInUser(for: basicCredentials()) + _ = try logInUser(for: basicCredentials()) let ex = expectation(description: "download-realm-async-open") - asyncOpen(user: user, appId: appId, partitionValue: #function) { asyncOpenState in + asyncOpen(appId: appId, openType: .pbs(#function)) { asyncOpenState in if case let .open(realm) = asyncOpenState { XCTAssertNotNil(realm) ex.fulfill() @@ -83,7 +99,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { executeChild() let ex = expectation(description: "download-populated-realm-async-open") - asyncOpen(user: user, appId: appId, partitionValue: #function) { asyncOpenState in + asyncOpen(appId: appId, openType: .pbs(#function)) { asyncOpenState in if case let .open(realm) = asyncOpenState { XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) @@ -97,7 +113,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { user.logOut { _ in } // Logout current user let ex = expectation(description: "download-realm-async-open-not-logged") - asyncOpen(user: user, appId: appId, partitionValue: #function) { asyncOpenState in + asyncOpen(appId: appId, openType: .pbs(#function)) { asyncOpenState in if case .waitingForUser = asyncOpenState { ex.fulfill() } @@ -115,11 +131,11 @@ class SwiftUIServerTests: SwiftSyncTestCase { localAppName: nil, localAppVersion: nil) let app = App(id: appId, configuration: appConfig) - let user = try logInUser(for: basicCredentials(app: app), app: app) + _ = try logInUser(for: basicCredentials(app: app), app: app) proxy.dropConnections = true let ex = expectation(description: "download-realm-async-open-no-connection") - asyncOpen(user: user, appId: appId, partitionValue: #function, timeout: 1000) { asyncOpenState in + asyncOpen(appId: appId, openType: .pbs(#function), timeout: 1000) { asyncOpenState in if case let .error(error) = asyncOpenState, let nsError = error as NSError? { XCTAssertEqual(nsError.code, Int(ETIMEDOUT)) @@ -141,7 +157,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { executeChild() let ex = expectation(description: "progress-async-open") - asyncOpen(user: user, appId: appId, partitionValue: #function) { asyncOpenState in + asyncOpen(appId: appId, openType: .pbs(#function)) { asyncOpenState in if case let .progress(progress) = asyncOpenState { XCTAssertTrue(progress.fractionCompleted > 0) if progress.isFinished { @@ -161,7 +177,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { executeChild() let ex = expectation(description: "download-cached-app-async-open") - asyncOpen(user: user, partitionValue: #function) { asyncOpenState in + asyncOpen(openType: .pbs(#function)) { asyncOpenState in if case let .open(realm) = asyncOpenState { XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) @@ -195,7 +211,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { executeChild() let ex = expectation(description: "download-partition-value-async-open") - asyncOpen(user: user, partitionValue: partitionValueA) { asyncOpenState in + asyncOpen(openType: .pbs(partitionValueA)) { asyncOpenState in if case let .open(realm) = asyncOpenState { XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) @@ -204,7 +220,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { } let ex2 = expectation(description: "download-other-partition-value-async-open") - asyncOpen(user: user, partitionValue: partitionValueB) { asyncOpenState in + asyncOpen(openType: .pbs(partitionValueB)) { asyncOpenState in if case let .open(realm) = asyncOpenState { XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) @@ -229,7 +245,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { executeChild() let ex = expectation(description: "test-multiuser1-app-async-open") - asyncOpen(user: syncUser2, appId: appId, partitionValue: partitionValueB) { asyncOpenState in + asyncOpen(appId: appId, openType: .pbs(partitionValueB)) { asyncOpenState in if case let .open(realm) = asyncOpenState { XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) @@ -242,7 +258,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { XCTAssertEqual(syncUser1.id, app.currentUser!.id) let ex2 = expectation(description: "test-multiuser2-app-async-open") - asyncOpen(user: syncUser2, appId: appId, partitionValue: partitionValueA) { asyncOpenState in + asyncOpen(appId: appId, openType: .pbs(partitionValueA)) { asyncOpenState in if case let .open(realm) = asyncOpenState { XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) @@ -262,9 +278,9 @@ class SwiftUIServerTests: SwiftSyncTestCase { } executeChild() - let anonymousUser = try logInUser(for: .anonymous) + _ = try logInUser(for: .anonymous) let ex = expectation(description: "download-realm-anonymous-user-async-open") - asyncOpen(user: anonymousUser, appId: appId, partitionValue: partitionValueA) { asyncOpenState in + asyncOpen(appId: appId, openType: .pbs(partitionValueA)) { asyncOpenState in if case let .open(realm) = asyncOpenState { XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) @@ -275,7 +291,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { app.currentUser?.logOut { _ in } // Logout anonymous user let ex2 = expectation(description: "download-realm-after-logout-async-open") - asyncOpen(user: user, appId: appId, partitionValue: partitionValueB) { asyncOpenState in + asyncOpen(appId: appId, openType: .pbs(partitionValueB)) { asyncOpenState in if case let .open(realm) = asyncOpenState { XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) @@ -284,13 +300,137 @@ class SwiftUIServerTests: SwiftSyncTestCase { } } + func testAsyncOpenFlexibleSyncInit() throws { + try populateFlexibleSyncData { realm in + for i in 1...10 { + // Using firstname to query only objects from this test + let person = SwiftPerson(firstName: "\(#function)", + lastName: "lastname_\(i)", + age: i) + realm.add(person) + } + } + + _ = try logInUser(for: basicCredentials(app: flexibleSyncApp), app: flexibleSyncApp) + + let ex = expectation(description: "download-realm-flexible-async-open") + asyncOpen(appId: flexibleSyncAppId, openType: .flexibleSync, timeout: 1000) { asyncOpenState in + if case let .open(realm) = asyncOpenState { + XCTAssertNotNil(realm) + XCTAssertTrue(realm.isEmpty) // should not have downloaded anything, because there are no subscriptions + ex.fulfill() + } + } + } + + func testAsyncOpenFlexibleSyncConfiguration() throws { + try populateFlexibleSyncData { realm in + for i in 1...10 { + // Using firstname to query only objects from this test + let person = SwiftPerson(firstName: "\(#function)", + lastName: "lastname_\(i)", + age: i) + realm.add(person) + } + } + + let user = try logInUser(for: basicCredentials(app: flexibleSyncApp), app: flexibleSyncApp) + + var configuration = user.flexibleSyncConfiguration(initialSubscriptions: { subs in + subs.append(QuerySubscription { + $0.firstName == "\(#function)" && $0.age > 0 + }) + }) + configuration.objectTypes = [SwiftPerson.self] + + let ex = expectation(description: "download-realm-flexible-async-open") + asyncOpen(appId: flexibleSyncAppId, openType: .configuration(configuration), timeout: 1000) { asyncOpenState in + if case let .open(realm) = asyncOpenState { + XCTAssertNotNil(realm) + self.checkCount(expected: 10, realm, SwiftPerson.self) + ex.fulfill() + } + } + } + + func testAsyncOpenPBSWithConfiguration() throws { + let user = try logInUser(for: basicCredentials()) + if !isParent { + populateRealm(user: user, partitionValue: #function) + return + } + executeChild() + + var configuration = user.configuration(partitionValue: #function) + configuration.objectTypes = [SwiftHugeSyncObject.self] + + let ex = expectation(description: "download-populated-realm-async-open") + asyncOpen(appId: appId, openType: .configuration(configuration)) { asyncOpenState in + if case let .open(realm) = asyncOpenState { + XCTAssertNotNil(realm) + self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) + ex.fulfill() + } + } + } + + func testAsyncOpenPBSWithClientReset() throws { + let user = try logInUser(for: basicCredentials()) + var configuration = user.configuration(partitionValue: #function, clientResetMode: .manual(errorHandler: { _, _ in })) + configuration.objectTypes = [SwiftPerson.self] + + let ex = expectation(description: "download-populated-realm-async-open") + asyncOpen(appId: appId, openType: .configuration(configuration)) { asyncOpenState in + if case let .open(realm) = asyncOpenState { + XCTAssertNotNil(realm) + switch configuration.syncConfiguration!.clientResetMode { + case .manual(let block): + XCTAssertNotNil(block) + default: + XCTFail("Should be set to manual") + } + ex.fulfill() + } + } + } + + func testAsyncOpenFlexibleSyncWithClientReset() throws { + let user = try logInUser(for: basicCredentials(app: flexibleSyncApp), app: flexibleSyncApp) + var configuration = user.flexibleSyncConfiguration(clientResetMode: .manual(errorHandler: { _, _ in })) + configuration.objectTypes = [SwiftPerson.self] + + let ex = expectation(description: "download-populated-realm-async-open") + asyncOpen(appId: flexibleSyncAppId, openType: .configuration(configuration)) { asyncOpenState in + if case let .open(realm) = asyncOpenState { + XCTAssertNotNil(realm) + switch configuration.syncConfiguration!.clientResetMode { + case .manual(let block): + XCTAssertNotNil(block) + default: + XCTFail("Should be set to manual") + } + ex.fulfill() + } + } + } + // MARK: - AutoOpen - func autoOpen(user: User, appId: String? = nil, partitionValue: String, timeout: UInt? = nil, handler: @escaping (AsyncOpenState) -> Void) { - let configuration = self.configuration(user: user, partition: partitionValue) - let autoOpen = AutoOpen(appId: appId, - partitionValue: partitionValue, + func autoOpen(appId: String? = nil, openType: OpenType, timeout: UInt? = nil, handler: @escaping (AsyncOpenState) -> Void) { + let autoOpen: AutoOpen + switch openType { + case .configuration(let configuration): + autoOpen = AutoOpen(appId: appId, configuration: configuration, timeout: timeout) + case .pbs(let partitionValue): + autoOpen = AutoOpen(appId: appId, + partitionValue: partitionValue, + timeout: timeout) + case .flexibleSync: + autoOpen = AutoOpen(appId: appId, + timeout: timeout) + } + _ = autoOpen.wrappedValue // Retrieving the wrappedValue to simulate a SwiftUI environment where this is called when initialising the view. autoOpen.projectedValue .sink(receiveValue: handler) @@ -300,10 +440,10 @@ class SwiftUIServerTests: SwiftSyncTestCase { } func testAutoOpenOpenRealm() throws { - let user = try logInUser(for: basicCredentials()) + _ = try logInUser(for: basicCredentials()) let ex = expectation(description: "download-realm-auto-open") - autoOpen(user: user, appId: appId, partitionValue: #function) { autoOpenState in + autoOpen(appId: appId, openType: .pbs(#function)) { autoOpenState in if case let .open(realm) = autoOpenState { XCTAssertNotNil(realm) ex.fulfill() @@ -321,7 +461,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { executeChild() let ex = expectation(description: "download-populated-realm-auto-open") - autoOpen(user: user, appId: appId, partitionValue: #function) { autoOpenState in + autoOpen(appId: appId, openType: .pbs(#function)) { autoOpenState in if case let .open(realm) = autoOpenState { XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) @@ -336,7 +476,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { user.logOut { _ in } // Logout current user let ex = expectation(description: "download-realm-auto-open-not-logged") - autoOpen(user: user, appId: appId, partitionValue: #function) { autoOpenState in + autoOpen(appId: appId, openType: .pbs(#function)) { autoOpenState in if case .waitingForUser = autoOpenState { ex.fulfill() } @@ -361,10 +501,10 @@ class SwiftUIServerTests: SwiftSyncTestCase { App.resetAppCache() let app = App(id: appId, configuration: appConfig) - let user = try logInUser(for: basicCredentials(app: app), app: app) + _ = try logInUser(for: basicCredentials(app: app), app: app) proxy.dropConnections = true let ex = expectation(description: "download-realm-auto-open-no-connection") - autoOpen(user: user, appId: appId, partitionValue: #function, timeout: 1000) { autoOpenState in + autoOpen(appId: appId, openType: .pbs(#function), timeout: 1000) { autoOpenState in if case let .open(realm) = autoOpenState { XCTAssertTrue(realm.isEmpty) // should not have downloaded anything ex.fulfill() @@ -398,27 +538,16 @@ class SwiftUIServerTests: SwiftSyncTestCase { App.resetAppCache() let app = App(id: flexibleSyncAppId, configuration: appConfig) - - let user = try logInUser(for: basicCredentials(app: app), app: app) - var configuration = user.flexibleSyncConfiguration() - configuration.objectTypes = [SwiftPerson.self] + _ = try logInUser(for: basicCredentials(app: app), app: app) proxy.dropConnections = true let ex = expectation(description: "download-realm-flexible-auto-open-no-connection") - let autoOpen = AutoOpen(appId: flexibleSyncAppId, configuration: configuration, timeout: 1000) - - _ = autoOpen.wrappedValue // Retrieving the wrappedValue to simulate a SwiftUI environment where this is called when initialising the view. - autoOpen.projectedValue - .sink { autoOpenState in - if case let .open(realm) = autoOpenState { - XCTAssertTrue(realm.isEmpty) // should not have downloaded anything - ex.fulfill() - } + autoOpen(appId: flexibleSyncAppId, openType: .flexibleSync, timeout: 1000) { autoOpenState in + if case let .open(realm) = autoOpenState { + XCTAssertTrue(realm.isEmpty) // should not have downloaded anything + ex.fulfill() } - .store(in: &cancellables) - - waitForExpectations(timeout: 10.0) - autoOpen.cancel() + } proxy.stop() } @@ -429,9 +558,9 @@ class SwiftUIServerTests: SwiftSyncTestCase { populateRealm(user: user, partitionValue: #function) } - let user = try logInUser(for: basicCredentials()) + _ = try logInUser(for: basicCredentials()) let ex = expectation(description: "progress-auto-open") - autoOpen(user: user, appId: appId, partitionValue: #function) { autoOpenState in + autoOpen(appId: appId, openType: .pbs(#function)) { autoOpenState in if case let .progress(progress) = autoOpenState { XCTAssertTrue(progress.fractionCompleted > 0) if progress.isFinished { @@ -451,7 +580,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { executeChild() let ex = expectation(description: "download-cached-app-auto-open") - autoOpen(user: user, partitionValue: #function) { autoOpenState in + autoOpen(openType: .pbs(#function)) { autoOpenState in if case let .open(realm) = autoOpenState { XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) @@ -491,7 +620,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { executeChild() let ex = expectation(description: "test-multiuser1-app-auto-open") - autoOpen(user: syncUser2, appId: appId, partitionValue: partitionValueB) { autoOpenState in + autoOpen(appId: appId, openType: .pbs(partitionValueB)) { autoOpenState in if case let .open(realm) = autoOpenState { XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) @@ -504,7 +633,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { XCTAssertEqual(syncUser1.id, app.currentUser!.id) let ex2 = expectation(description: "test-multiuser2-app-auto-open") - autoOpen(user: syncUser1, appId: appId, partitionValue: partitionValueA) { autoOpenState in + autoOpen(appId: appId, openType: .pbs(partitionValueA)) { autoOpenState in if case let .open(realm) = autoOpenState { XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) @@ -517,9 +646,9 @@ class SwiftUIServerTests: SwiftSyncTestCase { let partitionValueA = #function let partitionValueB = "\(#function)bar" - let anonymousUser = try logInUser(for: .anonymous) + _ = try logInUser(for: .anonymous) let ex = expectation(description: "download-realm-anonymous-user-auto-open") - autoOpen(user: anonymousUser, appId: appId, partitionValue: partitionValueA) { autoOpenState in + autoOpen(appId: appId, openType: .pbs(partitionValueA)) { autoOpenState in if case let .open(realm) = autoOpenState { XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) @@ -537,7 +666,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { executeChild() let ex2 = expectation(description: "download-realm-after-logout-auto-open") - autoOpen(user: user, appId: appId, partitionValue: partitionValueB) { autoOpenState in + autoOpen(appId: appId, openType: .pbs(partitionValueB)) { autoOpenState in if case let .open(realm) = autoOpenState { XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) @@ -558,7 +687,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { executeChild() let ex = expectation(description: "download-partition-value-auto-open") - autoOpen(user: user, partitionValue: partitionValueA) { autoOpenState in + autoOpen(openType: .pbs(partitionValueA)) { autoOpenState in if case let .open(realm) = autoOpenState { XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) @@ -567,7 +696,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { } let ex2 = expectation(description: "download-other-partition-value-auto-open") - autoOpen(user: user, partitionValue: partitionValueB) { autoOpenState in + autoOpen(openType: .pbs(partitionValueB)) { autoOpenState in if case let .open(realm) = autoOpenState { XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) @@ -589,7 +718,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { executeChild() let ex = expectation(description: "download-partition-value-async-open-mixed") - asyncOpen(user: user, partitionValue: partitionValueA) { asyncOpenState in + asyncOpen(openType: .pbs(partitionValueA)) { asyncOpenState in if case let .open(realm) = asyncOpenState { XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) @@ -598,7 +727,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { } let ex2 = expectation(description: "download-partition-value-auto-open-mixed") - autoOpen(user: user, partitionValue: partitionValueB) { autoOpenState in + autoOpen(openType: .pbs(partitionValueB)) { autoOpenState in if case let .open(realm) = autoOpenState { XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) @@ -623,7 +752,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { executeChild() let ex = expectation(description: "test-combine-multiuser1-app-auto-open") - autoOpen(user: syncUser2, appId: appId, partitionValue: partitionValueB) { autoOpenState in + autoOpen(appId: appId, openType: .pbs(partitionValueB)) { autoOpenState in if case let .open(realm) = autoOpenState { XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) @@ -636,7 +765,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { XCTAssertEqual(syncUser1.id, app.currentUser!.id) let ex2 = expectation(description: "test-combine-multiuser2-app-auto-open") - asyncOpen(user: syncUser1, appId: appId, partitionValue: partitionValueA) { asyncOpenState in + asyncOpen(appId: appId, openType: .pbs(partitionValueA)) { asyncOpenState in if case let .open(realm) = asyncOpenState { XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) @@ -644,4 +773,118 @@ class SwiftUIServerTests: SwiftSyncTestCase { } } } + + func testAutoOpenFlexibleSyncInit() throws { + try populateFlexibleSyncData { realm in + for i in 1...10 { + // Using firstname to query only objects from this test + let person = SwiftPerson(firstName: "\(#function)", + lastName: "lastname_\(i)", + age: i) + realm.add(person) + } + } + + _ = try logInUser(for: basicCredentials(app: flexibleSyncApp), app: flexibleSyncApp) + + let ex = expectation(description: "download-realm-flexible-async-open") + autoOpen(appId: flexibleSyncAppId, openType: .flexibleSync, timeout: 1000) { autoOpenState in + if case let .open(realm) = autoOpenState { + XCTAssertNotNil(realm) + XCTAssertTrue(realm.isEmpty) // should not have downloaded anything, because there are no subscriptions + ex.fulfill() + } + } + } + + func testAutoOpenFlexibleSyncConfiguration() throws { + try populateFlexibleSyncData { realm in + for i in 1...10 { + // Using firstname to query only objects from this test + let person = SwiftPerson(firstName: "\(#function)", + lastName: "lastname_\(i)", + age: i) + realm.add(person) + } + } + + let user = try logInUser(for: basicCredentials(app: flexibleSyncApp), app: flexibleSyncApp) + + var configuration = user.flexibleSyncConfiguration(initialSubscriptions: { subs in + subs.append(QuerySubscription { + $0.firstName == "\(#function)" && $0.age > 0 + }) + }) + configuration.objectTypes = [SwiftPerson.self] + + let ex = expectation(description: "download-realm-flexible-async-open") + autoOpen(appId: flexibleSyncAppId, openType: .configuration(configuration), timeout: 1000) { autoOpenState in + if case let .open(realm) = autoOpenState { + XCTAssertNotNil(realm) + self.checkCount(expected: 10, realm, SwiftPerson.self) + ex.fulfill() + } + } + } + + func testAutoOpenPBSWithConfiguration() throws { + let user = try logInUser(for: basicCredentials()) + if !isParent { + populateRealm(user: user, partitionValue: #function) + return + } + executeChild() + + var configuration = user.configuration(partitionValue: #function) + configuration.objectTypes = [SwiftHugeSyncObject.self] + + let ex = expectation(description: "download-populated-realm-async-open") + autoOpen(appId: appId, openType: .configuration(configuration)) { autoOpenState in + if case let .open(realm) = autoOpenState { + XCTAssertNotNil(realm) + self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) + ex.fulfill() + } + } + } + + func testAutoOpenPBSWithClientReset() throws { + let user = try logInUser(for: basicCredentials()) + var configuration = user.configuration(partitionValue: #function, clientResetMode: .manual(errorHandler: { _, _ in })) + configuration.objectTypes = [SwiftPerson.self] + + let ex = expectation(description: "download-populated-realm-async-open") + autoOpen(appId: appId, openType: .configuration(configuration)) { autoOpenState in + if case let .open(realm) = autoOpenState { + XCTAssertNotNil(realm) + switch configuration.syncConfiguration!.clientResetMode { + case .manual(let block): + XCTAssertNotNil(block) + default: + XCTFail("Should be set to manual") + } + ex.fulfill() + } + } + } + + func testAutoOpenFlexibleSyncWithClientReset() throws { + let user = try logInUser(for: basicCredentials(app: flexibleSyncApp), app: flexibleSyncApp) + var configuration = user.flexibleSyncConfiguration(clientResetMode: .manual(errorHandler: { _, _ in })) + configuration.objectTypes = [SwiftPerson.self] + + let ex = expectation(description: "download-populated-realm-async-open") + autoOpen(appId: flexibleSyncAppId, openType: .configuration(configuration)) { autoOpenState in + if case let .open(realm) = autoOpenState { + XCTAssertNotNil(realm) + switch configuration.syncConfiguration!.clientResetMode { + case .manual(let block): + XCTAssertNotNil(block) + default: + XCTFail("Should be set to manual") + } + ex.fulfill() + } + } + } } diff --git a/Realm/Tests/SwiftUISyncTestHost/ContentView.swift b/Realm/Tests/SwiftUISyncTestHost/ContentView.swift index f37affb810..877f08eb96 100644 --- a/Realm/Tests/SwiftUISyncTestHost/ContentView.swift +++ b/Realm/Tests/SwiftUISyncTestHost/ContentView.swift @@ -90,7 +90,16 @@ struct MainView: View { .transition(AnyTransition.move(edge: .leading)).animation(.default) case "async_open_flexible_sync": AsyncOpenFlexibleSyncView() - .environment(\.realmConfiguration, user!.flexibleSyncConfiguration()) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.green) + .transition(AnyTransition.move(edge: .leading)).animation(.default) + case "async_open_flexible_sync_configuration": + AsyncOpenFlexibleSyncView(subscriptionState: .completed) + .environment(\.realmConfiguration, user!.flexibleSyncConfiguration(initialSubscriptions: { subs in + subs.append(QuerySubscription(name: "person_age") { + $0.age > 5 && $0.firstName == ProcessInfo.processInfo.environment["firstName"]! + }) + })) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.green) .transition(AnyTransition.move(edge: .leading)).animation(.default) @@ -119,7 +128,16 @@ struct MainView: View { .transition(AnyTransition.move(edge: .leading)).animation(.default) case "auto_open_flexible_sync": AutoOpenFlexibleSyncView() - .environment(\.realmConfiguration, user!.flexibleSyncConfiguration()) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.green) + .transition(AnyTransition.move(edge: .leading)).animation(.default) + case "auto_open_flexible_sync_configuration": + AutoOpenFlexibleSyncView(subscriptionState: .completed) + .environment(\.realmConfiguration, user!.flexibleSyncConfiguration(initialSubscriptions: { subs in + subs.append(QuerySubscription(name: "person_age") { + $0.age > 2 && $0.firstName == ProcessInfo.processInfo.environment["firstName"]! + }) + })) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.green) .transition(AnyTransition.move(edge: .leading)).animation(.default) @@ -197,6 +215,7 @@ class LoginHelper: ObservableObject { func login(email: String, password: String, completion: @escaping (User) -> Void) { let app = RealmSwift.App(id: ProcessInfo.processInfo.environment["app_id"]!, configuration: appConfig) + app.syncManager.logLevel = .trace app.login(credentials: .emailPassword(email: email, password: password)) .receive(on: DispatchQueue.main) .sink(receiveCompletion: { result in @@ -417,12 +436,16 @@ struct AsyncOpenFlexibleSyncView: View { Task { do { let subs = realm.subscriptions - try await subs.update { - subs.append(QuerySubscription(name: "person_age") { - $0.age > 5 && $0.firstName == ProcessInfo.processInfo.environment["firstName"]! - }) + do { + try await subs.update { + subs.append(QuerySubscription(name: "person_age") { + $0.age > 5 && $0.firstName == ProcessInfo.processInfo.environment["firstName"]! + }) + } + subscriptionState = .completed + } catch { + print(error) } - subscriptionState = .completed } } } diff --git a/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift b/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift index d9e746ea9b..6beaedffa5 100644 --- a/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift +++ b/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift @@ -552,12 +552,41 @@ extension SwiftUISyncTestHostUITests { XCTAssertTrue(nextViewView.waitForExistence(timeout: 10)) nextViewView.tap() + // Test show ListView after syncing realm + let table = application.tables.firstMatch + XCTAssertTrue(table.waitForExistence(timeout: 6)) + XCTAssertEqual(table.cells.count, 8) + } + + func testFlexibleSyncAsyncOpenWithEnvironmentConfiguration() throws { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + try populateFlexibleSyncForEmail(email, n: 10) { realm in + for index in (1...10) { + realm.add(SwiftPerson(firstName: "\(#function)", lastName: "Smith", age: index)) + } + } + + application.launchEnvironment["email1"] = email + application.launchEnvironment["async_view_type"] = "async_open_flexible_sync_configuration" + // Override appId for flexible app Id + application.launchEnvironment["app_id"] = flexibleSyncAppId + application.launchEnvironment["firstName"] = "\(#function)" + application.launch() + + asyncOpen() + + // Query for button to navigate to next view + let nextViewView = application.buttons["show_list_button_view"] + XCTAssertTrue(nextViewView.waitForExistence(timeout: 10)) + nextViewView.tap() + // Test show ListView after syncing realm let table = application.tables.firstMatch XCTAssertTrue(table.waitForExistence(timeout: 6)) XCTAssertEqual(table.cells.count, 5) } + func testFlexibleSyncAutoOpenRoundTrip() throws { let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" try populateFlexibleSyncForEmail(email, n: 10) { realm in @@ -585,4 +614,32 @@ extension SwiftUISyncTestHostUITests { XCTAssertTrue(table.waitForExistence(timeout: 6)) XCTAssertEqual(table.cells.count, 18) } + + func testFlexibleSyncAutoOpenWithEnvironmentConfiguration() throws { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + try populateFlexibleSyncForEmail(email, n: 10) { realm in + for index in (1...20) { + realm.add(SwiftPerson(firstName: "\(#function)", lastName: "Smith", age: index)) + } + } + + application.launchEnvironment["email1"] = email + application.launchEnvironment["async_view_type"] = "auto_open_flexible_sync_configuration" + // Override appId for flexible app Id + application.launchEnvironment["app_id"] = flexibleSyncAppId + application.launchEnvironment["firstName"] = "\(#function)" + application.launch() + + asyncOpen() + + // Query for button to navigate to next view + let nextViewView = application.buttons["show_list_button_view"] + XCTAssertTrue(nextViewView.waitForExistence(timeout: 10)) + nextViewView.tap() + + // Test show ListView after syncing realm + let table = application.tables.firstMatch + XCTAssertTrue(table.waitForExistence(timeout: 6)) + XCTAssertEqual(table.cells.count, 18) + } } diff --git a/RealmSwift/SwiftUI.swift b/RealmSwift/SwiftUI.swift index 6cc921877b..cd31c214a0 100644 --- a/RealmSwift/SwiftUI.swift +++ b/RealmSwift/SwiftUI.swift @@ -1523,22 +1523,23 @@ private class ObservableAsyncOpenStorage: ObservableObject { private func asyncOpenForUser(_ user: User) { // Set the `syncConfiguration` depending if there is partition value (pbs) or not (flx). var config: Realm.Configuration - if let partitionValue = partitionValue { - config = user.configuration(partitionValue: partitionValue, cancelAsyncOpenOnNonFatalErrors: true) - } else { - config = user.flexibleSyncConfiguration(cancelAsyncOpenOnNonFatalErrors: true) - } - // Use the user configuration by default or set configuration with the current user `syncConfiguration`'s. - if var configuration = configuration { + if let configuration = configuration { // We want to throw if the configuration doesn't contain a `SyncConfiguration` guard configuration.syncConfiguration != nil else { throwRealmException("The used configuration was not configured with sync.") } - let userSyncConfig = config.syncConfiguration - configuration.syncConfiguration = userSyncConfig config = configuration + } else { + if let partitionValue = partitionValue { + config = user.configuration(partitionValue: partitionValue) + } else { + config = user.flexibleSyncConfiguration() + } } + let syncConfiguration = config.syncConfiguration + syncConfiguration?.config.cancelAsyncOpenOnNonFatalErrors = true + config.syncConfiguration = syncConfiguration // Cancel any current subscriptions to asyncOpen if there is one cancelAsyncOpen() @@ -1694,15 +1695,16 @@ private class ObservableAsyncOpenStorage: ObservableObject { } /** - Initialize the property wrapper + Initialize the property wrapper for a given configuration or Partition. - parameter appId: The unique identifier of your Realm app, if empty or `nil` will try to retrieve latest singular cached app. - parameter partitionValue: The `BSON` value the Realm is partitioned on. - - parameter configuration: The `Realm.Configuration` used when creating the Realm, - user's sync configuration for the given partition value will be set as the `syncConfiguration`, - if empty the user configuration will be used. + - parameter configuration: A configuration `Realm.configuration` to use when opening the Realm. - parameter timeout: The maximum number of milliseconds to allow for a connection to become fully established., if empty or `nil` no connection timeout is set. + + - note: This intialiser will use either the configuration or build a configuration from the partition value using `user.configuration(partitionValue:)`. */ + @available(*, deprecated, message: "This API will be deprecated. Use init(appId:partitionValue:timeout) if you want are connecting to a partition based sync without a configuration, or init(appId:configuration:timeout) in case you are using a configuration") public init(appId: String? = nil, partitionValue: Partition, configuration: Realm.Configuration? = nil, @@ -1713,11 +1715,9 @@ private class ObservableAsyncOpenStorage: ObservableObject { } /** - Initialize the property wrapper for a flexible sync configuration. + Initialize the property wrapper with a configuration. - parameter appId: The unique identifier of your Realm app, if empty or `nil` will try to retrieve latest singular cached app. - - parameter configuration: The `Realm.Configuration` used when creating the Realm, - user's sync configuration for the given partition value will be set as the `syncConfiguration`, - if empty the user configuration will be used. + - parameter configuration: A configuration `Realm.configuration` to use when opening the Realm. - parameter timeout: The maximum number of milliseconds to allow for a connection to become fully established., if empty or `nil` no connection timeout is set. */ @@ -1729,6 +1729,34 @@ private class ObservableAsyncOpenStorage: ObservableObject { storage = ObservableAsyncOpenStorage(asyncOpenKind: .asyncOpen, app: app, configuration: configuration, partitionValue: nil) } + /** + Initialize the property wrapper for a partition sync based sync app. + - parameter appId: The unique identifier of your Realm app, if empty or `nil` will try to retrieve latest singular cached app. + - parameter partitionValue: The `BSON` value the Realm is partitioned on. + - parameter timeout: The maximum number of milliseconds to allow for a connection to + become fully established., if empty or `nil` no connection timeout is set. + */ + public init(appId: String? = nil, + partitionValue: Partition, + timeout: UInt? = nil) where Partition: BSON { + let app = ObservableAsyncOpenStorage.configureApp(appId: appId, timeout: timeout) + // Store property wrapper values on the storage + storage = ObservableAsyncOpenStorage(asyncOpenKind: .asyncOpen, app: app, configuration: nil, partitionValue: AnyBSON(partitionValue)) + } + + /** + Initialize the property wrapper for a flexible sync app. + - parameter appId: The unique identifier of your Realm app, if empty or `nil` will try to retrieve latest singular cached app. + - parameter timeout: The maximum number of milliseconds to allow for a connection to + become fully established., if empty or `nil` no connection timeout is set. + */ + public init(appId: String? = nil, + timeout: UInt? = nil) { + let app = ObservableAsyncOpenStorage.configureApp(appId: appId, timeout: timeout) + // Store property wrapper values on the storage + storage = ObservableAsyncOpenStorage(asyncOpenKind: .asyncOpen, app: app, configuration: nil, partitionValue: nil) + } + nonisolated public func update() { unsafeInvokeAsMainActor { storage.update(partitionValue, configuration) @@ -1818,15 +1846,16 @@ func unsafeInvokeAsMainActor(_ fn: @MainActor () -> Void) { } /** - Initialize the property wrapper - - parameter appId: The unique identifier of your Realm app, if empty or `nil` will try to retrieve latest singular cached app. + Initialize the property wrapper for a given configuration or Partition. + - parameter appId: The unique identifier of your Realm app, if empty or `nil` will try to retrieve latest singular cached app. - parameter partitionValue: The `BSON` value the Realm is partitioned on. - - parameter configuration: The `Realm.Configuration` used when creating the Realm, - user's sync configuration for the given partition value will be set as the `syncConfiguration`, - if empty the user configuration will be used. + - parameter configuration: A configuration `Realm.configuration` to use when opening the Realm. - parameter timeout: The maximum number of milliseconds to allow for a connection to - become fully established, if empty or `nil` no connection timeout is set. + become fully established., if empty or `nil` no connection timeout is set. + + - note: This intialiser will use either the configuration or build a configuration from the partition value using `user.configuration(partitionValue:)`. */ + @available(*, deprecated, message: "This API will be deprecated. Use init(appId:partitionValue:timeout) if you want are connecting to a partition based sync without a configuration, or init(appId:configuration:timeout) in case you are using a configuration") public init(appId: String? = nil, partitionValue: Partition, configuration: Realm.Configuration? = nil, @@ -1837,11 +1866,9 @@ func unsafeInvokeAsMainActor(_ fn: @MainActor () -> Void) { } /** - Initialize the property wrapper for a flexible sync configuration. + Initialize the property wrapper with a configuration. - parameter appId: The unique identifier of your Realm app, if empty or `nil` will try to retrieve latest singular cached app. - - parameter configuration: The `Realm.Configuration` used when creating the Realm, - user's sync configuration for the given partition value will be set as the `syncConfiguration`, - if empty the user configuration will be used. + - parameter configuration: A configuration `Realm.configuration` to use when opening the Realm. - parameter timeout: The maximum number of milliseconds to allow for a connection to become fully established., if empty or `nil` no connection timeout is set. */ @@ -1853,6 +1880,35 @@ func unsafeInvokeAsMainActor(_ fn: @MainActor () -> Void) { storage = ObservableAsyncOpenStorage(asyncOpenKind: .autoOpen, app: app, configuration: configuration, partitionValue: nil) } + /** + Initialize the property wrapper for a partition sync based sync app. + - parameter appId: The unique identifier of your Realm app, if empty or `nil` will try to retrieve latest singular cached app. + - parameter partitionValue: The `BSON` value the Realm is partitioned on. + - parameter timeout: The maximum number of milliseconds to allow for a connection to + become fully established., if empty or `nil` no connection timeout is set. + */ + public init(appId: String? = nil, + partitionValue: Partition, + timeout: UInt? = nil) where Partition: BSON { + let app = ObservableAsyncOpenStorage.configureApp(appId: appId, timeout: timeout) + // Store property wrapper values on the storage + storage = ObservableAsyncOpenStorage(asyncOpenKind: .autoOpen, app: app, configuration: nil, partitionValue: AnyBSON(partitionValue)) + } + + /** + Initialize the property wrapper for a flexible sync app. + - parameter appId: The unique identifier of your Realm app, if empty or `nil` will try to retrieve latest singular cached app. + - parameter timeout: The maximum number of milliseconds to allow for a connection to + become fully established., if empty or `nil` no connection timeout is set. + */ + public init(appId: String? = nil, + timeout: UInt? = nil) { + let app = ObservableAsyncOpenStorage.configureApp(appId: appId, timeout: timeout) + // Store property wrapper values on the storage + storage = ObservableAsyncOpenStorage(asyncOpenKind: .autoOpen, app: app, configuration: nil, partitionValue: nil) + } + + nonisolated public func update() { unsafeInvokeAsMainActor { storage.update(partitionValue, configuration)