diff --git a/.swiftlint.yml b/.swiftlint.yml index 490a2099f8b..2b195648604 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -42,6 +42,7 @@ identifier_name: - _rlmSetAccessor(_:) - _rlmSetProperty(_:_:_:) - _rlmType + - _rlmSyncSubscription - id - pk - to diff --git a/Realm/ObjectServerTests/RLMSyncTestCase.mm b/Realm/ObjectServerTests/RLMSyncTestCase.mm index 8bb34f64079..7a2be013b37 100644 --- a/Realm/ObjectServerTests/RLMSyncTestCase.mm +++ b/Realm/ObjectServerTests/RLMSyncTestCase.mm @@ -626,7 +626,7 @@ - (NSString *)flexibleSyncAppId { } else { NSError *error; - _flexibleSyncAppId = [RealmServer.shared createAppWithQueryableFields:@[@"age", @"breed", @"partition", @"firstName", @"boolCol", @"intCol", @"stringCol", @"dateCol", @"lastName"] error:&error]; + _flexibleSyncAppId = [RealmServer.shared createAppWithQueryableFields:@[@"age", @"breed", @"partition", @"firstName", @"name", @"species", @"lastName"] error:&error]; if (error) { NSLog(@"Failed to create app: %@", error); abort(); diff --git a/Realm/ObjectServerTests/RLMUser+ObjectServerTests.h b/Realm/ObjectServerTests/RLMUser+ObjectServerTests.h index 2d463727ffb..afb32fe3e78 100644 --- a/Realm/ObjectServerTests/RLMUser+ObjectServerTests.h +++ b/Realm/ObjectServerTests/RLMUser+ObjectServerTests.h @@ -25,6 +25,9 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)waitForUploadToFinish:(NSString *)partitionValue; - (BOOL)waitForDownloadToFinish:(NSString *)partitionValue; +- (BOOL)waitForUploadsForRealm:(RLMRealm *)realm error:(NSError **)error; +- (BOOL)waitForDownloadsForRealm:(RLMRealm *)realm error:(NSError **)error; + - (void)simulateClientResetErrorForSession:(NSString *)partitionValue; @end diff --git a/Realm/ObjectServerTests/RLMUser+ObjectServerTests.mm b/Realm/ObjectServerTests/RLMUser+ObjectServerTests.mm index 56e6795694c..89aeb54ab6e 100644 --- a/Realm/ObjectServerTests/RLMUser+ObjectServerTests.mm +++ b/Realm/ObjectServerTests/RLMUser+ObjectServerTests.mm @@ -58,6 +58,45 @@ - (BOOL)waitForDownloadToFinish:(NSString *)partitionValue { return dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC))) == 0; } +- (BOOL)waitForUploadsForRealm:(RLMRealm *)realm error:(NSError **)error { + const NSTimeInterval timeout = 20; + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + RLMSyncSession *session = realm.syncSession; + NSAssert(session, @"Cannot call with invalid Realm"); + __block NSError *completionError; + BOOL couldWait = [session waitForUploadCompletionOnQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0) callback:^(NSError *error) { + completionError = error; + dispatch_semaphore_signal(sema); + }]; + if (!couldWait) { + return NO; + } + + if (error) + *error = completionError; + + return dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC))) == 0; +} + +- (BOOL)waitForDownloadsForRealm:(RLMRealm *)realm error:(NSError **)error { + const NSTimeInterval timeout = 20; + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + RLMSyncSession *session = realm.syncSession; + NSAssert(session, @"Cannot call with invalid Realm"); + __block NSError *completionError; + BOOL couldWait = [session waitForDownloadCompletionOnQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0) callback:^(NSError *error) { + completionError = error; + }]; + if (!couldWait) { + return NO; + } + + if (error) + *error = completionError; + + return dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC))) == 0; +} + - (void)simulateClientResetErrorForSession:(NSString *)partitionValue { RLMSyncSession *session = [self sessionForPartitionValue:partitionValue]; NSAssert(session, @"Cannot call with invalid URL"); diff --git a/Realm/ObjectServerTests/RealmServer.swift b/Realm/ObjectServerTests/RealmServer.swift index cef033fc328..9d64cc68b14 100644 --- a/Realm/ObjectServerTests/RealmServer.swift +++ b/Realm/ObjectServerTests/RealmServer.swift @@ -794,7 +794,7 @@ public class RealmServer: NSObject { } else { // This is a temporary workaround for not been able to add the complete schema for a flx App syncTypes = schema.objectSchema.filter { - let validSyncClasses = ["Dog", "Person", "SwiftPerson", "SwiftTypesSyncObject"] + let validSyncClasses = ["Dog", "Person", "SwiftPerson", "SwiftDog", "Bird"] return validSyncClasses.contains($0.className) } partitionKeyType = nil diff --git a/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift b/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift index 2b805b30f15..34d0634c8b4 100644 --- a/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift +++ b/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift @@ -28,6 +28,16 @@ import SwiftUI #endif class SwiftFlexibleSyncTests: SwiftSyncTestCase { + override class var defaultTestSuite: XCTestSuite { + // async/await is currently incompatible with thread sanitizer and will + // produce many false positives + // https://bugs.swift.org/browse/SR-15444 + if RLMThreadSanitizerEnabled() { + return XCTestSuite(name: "\(type(of: self))") + } + return super.defaultTestSuite + } + func testCreateFlexibleSyncApp() throws { let appId = try RealmServer.shared.createAppForSyncMode(.flx(["age"])) let flexibleApp = app(fromAppId: appId) @@ -35,11 +45,6 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { XCTAssertNotNil(user) } - func testFlexibleSyncOpenRealm() throws { - let realm = try openFlexibleSyncRealm() - XCTAssertNotNil(realm) - } - func testGetSubscriptionsWhenLocalRealm() throws { let realm = try Realm() assertThrows(realm.subscriptions) @@ -52,999 +57,455 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { assertThrows(realm.subscriptions) } - func testFlexibleSyncPath() throws { + func testOpenFlexibleSyncPath() throws { let user = try logInUser(for: basicCredentials(app: flexibleSyncApp), app: flexibleSyncApp) - let config = user.flexibleSyncConfiguration() - XCTAssertTrue(config.fileURL!.path.hasSuffix("mongodb-realm/\(flexibleSyncAppId)/\(user.id)/flx_sync_default.realm")) + let realm = try user.realm() + XCTAssertTrue(realm.configuration.fileURL!.path.hasSuffix("mongodb-realm/\(flexibleSyncAppId)/\(user.id)/flx_sync_default.realm")) } +} - func testGetSubscriptions() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - XCTAssertEqual(subscriptions.count, 0) - } +// MARK: - Async Await - func testWriteEmptyBlock() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - } +#if swift(>=5.6) && canImport(_Concurrency) +@available(macOS 12.0, *) +extension SwiftFlexibleSyncTests { + @MainActor + private func populateFlexibleSyncDataForType(_ type: T.Type, app: RealmSwift.App? = nil, block: @escaping (Realm) -> Void) async throws { + let app = app ?? flexibleSyncApp + let user = try await app.login(credentials: basicCredentials(usernameSuffix: "", app: app)) + let configuration = Realm.Configuration() + let realm = try user.realm(configuration: configuration) - XCTAssertEqual(subscriptions.count, 0) - } + _ = try await realm.subscriptions.subscribe(to: type) - func testAddOneSubscriptionWithoutName() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append(QuerySubscription { - $0.age > 15 - }) + try realm.write { + block(realm) } - - XCTAssertEqual(subscriptions.count, 1) + waitForUploads(for: realm) } - func testAddOneSubscriptionWithName() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append(QuerySubscription(name: "person_age") { - $0.age > 15 - }) + @MainActor + func testFlexibleSyncResults() async throws { + try await populateFlexibleSyncDataForType(SwiftPerson.self) { realm in + for i in 1...21 { + let person = SwiftPerson(firstName: "\(#function)", lastName: "lastname_\(i)", age: i) + realm.add(person) + } } - XCTAssertEqual(subscriptions.count, 1) - } + let user = try await self.flexibleSyncApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleSyncApp)) + let realm = try user.realm(configuration: Realm.Configuration()) + let persons: QueryResults = try await realm.subscriptions.subscribe(to: { $0.age > 18 && $0.firstName == "\(#function)" }) + XCTAssertEqual(persons.count, 3) - func testAddSubscriptionsInDifferentBlocks() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append(QuerySubscription(name: "person_age") { - $0.age > 15 - }) - } - subscriptions.write { - subscriptions.append(QuerySubscription { - $0.boolCol == true - }) - } + // This will trigger a client reset, which will result in the server not responding to any instruction, this is not removing the object from the database. +// let newPerson = SwiftPerson() +// newPerson.age = 10 +// try realm.write { +// realm.add(newPerson) +// } +// XCTAssertEqual(persons.count, 3) - XCTAssertEqual(subscriptions.count, 2) - } - - func testAddSeveralSubscriptionsWithoutName() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append( - QuerySubscription { - $0.age > 15 - }, - QuerySubscription { - $0.age > 20 - }, - QuerySubscription { - $0.age > 25 - }) + let newPerson = SwiftPerson(firstName: "\(#function)", lastName: "", age: 19) + try realm.write { + realm.add(newPerson) } + XCTAssertEqual(persons.count, 4) - XCTAssertEqual(subscriptions.count, 3) + try await persons.unsubscribe() + XCTAssertEqual(persons.count, 0) } - func testAddSeveralSubscriptionsWithName() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append( - QuerySubscription(name: "person_age_15") { - $0.age > 15 - }, - QuerySubscription(name: "person_age_20") { - $0.age > 20 - }, - QuerySubscription(name: "person_age_25") { - $0.age > 25 - }) - } - XCTAssertEqual(subscriptions.count, 3) - } + @MainActor + func testFlexibleSyncResultsForAllCollection() async throws { + let appId = try RealmServer.shared.createAppForSyncMode(.flx([])) - func testAddMixedSubscriptions() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append(QuerySubscription(name: "person_age_15") { - $0.age > 15 - }) - subscriptions.append( - QuerySubscription { - $0.boolCol == true - }, - QuerySubscription(name: "object_date_now") { - $0.dateCol <= Date() - }) + let flexibleApp = app(fromAppId: appId) + try await populateFlexibleSyncDataForType(SwiftPerson.self, app: flexibleApp) { realm in + for i in 1...9 { + let person = SwiftPerson(firstName: "\(#function)", lastName: "lastname_\(i)", age: i) + realm.add(person) + } } - XCTAssertEqual(subscriptions.count, 3) - } - - func testAddDuplicateSubscriptions() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append( - QuerySubscription { - $0.age > 15 - }, - QuerySubscription { - $0.age > 15 - }) + try await populateFlexibleSyncDataForType(SwiftDog.self, app: flexibleApp) { realm in + for _ in 1...8 { + let dog = SwiftDog(name: "\(#function)", breed: ["bulldog", "poodle", "boxer", "beagle"].randomElement()!) + realm.add(dog) + } } - XCTAssertEqual(subscriptions.count, 1) - } - func testAddDuplicateSubscriptionWithDifferentName() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append( - QuerySubscription(name: "person_age_1") { - $0.age > 15 - }, - QuerySubscription(name: "person_age_2") { - $0.age > 15 - }) - } - XCTAssertEqual(subscriptions.count, 2) - } + let user = try await flexibleApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleApp)) + let realm = try user.realm(configuration: Realm.Configuration()) + let (persons, dogs) = try await realm.subscriptions.subscribe(to: QuerySubscription(), QuerySubscription()) - // FIXME: Using `assertThrows` within a Server test will crash on tear down - func skip_testSameNamedSubscriptionThrows() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append(QuerySubscription(name: "person_age_1") { - $0.age > 15 - }) - assertThrows(subscriptions.append(QuerySubscription(name: "person_age_1") { - $0.age > 20 - })) - } - XCTAssertEqual(subscriptions.count, 1) - } + XCTAssertEqual(persons.count, 9) + XCTAssertEqual(dogs.count, 8) + XCTAssertEqual(realm.subscriptions.count, 2) - // FIXME: Using `assertThrows` within a Server test will crash on tear down - func skip_testAddSubscriptionOutsideWriteThrows() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - assertThrows(subscriptions.append(QuerySubscription(name: "person_age_1") { - $0.age > 15 - })) + try await persons.unsubscribe() + try await dogs.unsubscribe() } - func testFindSubscriptionByName() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append( - QuerySubscription(name: "person_age_15") { - $0.age > 15 - }, - QuerySubscription(name: "person_age_20") { - $0.age > 20 - }) + @MainActor + func testFlexibleSyncResultsWithDuplicateQuery() async throws { + try await populateFlexibleSyncDataForType(SwiftPerson.self) { realm in + for i in 1...21 { + let person = SwiftPerson(firstName: "\(#function)", lastName: "lastname_\(i)", age: i) + realm.add(person) + } } - XCTAssertEqual(subscriptions.count, 2) - let foundSubscription1 = subscriptions.first(named: "person_age_15") - XCTAssertNotNil(foundSubscription1) - XCTAssertEqual(foundSubscription1!.name, "person_age_15") + let user = try await self.flexibleSyncApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleSyncApp)) + let realm = try user.realm(configuration: Realm.Configuration()) - let foundSubscription2 = subscriptions.first(named: "person_age_20") - XCTAssertNotNil(foundSubscription2) - XCTAssertEqual(foundSubscription2!.name, "person_age_20") - } + let persons: QueryResults = try await realm.subscriptions.subscribe(to: { $0.age > 18 && $0.firstName == "\(#function)" }) + XCTAssertEqual(persons.count, 3) + let persons2: QueryResults = try await realm.subscriptions.subscribe(to: { $0.age > 18 && $0.firstName == "\(#function)" }) + XCTAssertEqual(persons2.count, 3) - func testFindSubscriptionByQuery() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append(QuerySubscription(name: "person_firstname_james") { - $0.firstName == "James" - }) - subscriptions.append(QuerySubscription(name: "object_int_more_than_zero") { - $0.intCol > 0 - }) - } - XCTAssertEqual(subscriptions.count, 2) - - let foundSubscription1 = subscriptions.first(ofType: SwiftPerson.self, where: { - $0.firstName == "James" - }) - XCTAssertNotNil(foundSubscription1) - XCTAssertEqual(foundSubscription1!.name, "person_firstname_james") - - let foundSubscription2 = subscriptions.first(ofType: SwiftTypesSyncObject.self, where: { - $0.intCol > 0 - }) - XCTAssertNotNil(foundSubscription2) - XCTAssertEqual(foundSubscription2!.name, "object_int_more_than_zero") - } - - func testRemoveSubscriptionByName() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append(QuerySubscription(name: "person_firstname_james") { - $0.firstName == "James" - }) - subscriptions.append( - QuerySubscription(name: "object_int_more_than_zero") { - $0.intCol > 0 - }, - QuerySubscription(name: "object_string") { - $0.stringCol == "John" || $0.stringCol == "Tom" - }) - } - XCTAssertEqual(subscriptions.count, 3) + // The results are pointing to the same subscription, which means the data on both will be the same + XCTAssertEqual(realm.subscriptions.count, 1) + XCTAssertEqual(persons.count, persons2.count) - subscriptions.write { - subscriptions.remove(named: "person_firstname_james") + let newPerson = SwiftPerson(firstName: "\(#function)", lastName: "", age: 19) + try realm.write { + realm.add(newPerson) } - XCTAssertEqual(subscriptions.count, 2) - } + XCTAssertEqual(persons.count, 4) + XCTAssertEqual(persons2.count, 4) + XCTAssertEqual(persons.count, persons2.count) + XCTAssertEqual(realm.subscriptions.count, 1) - func testRemoveSubscriptionByQuery() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append( - QuerySubscription { - $0.firstName == "Alex" - }, - QuerySubscription { - $0.firstName == "Belle" - }, - QuerySubscription { - $0.firstName == "Charles" - }) - subscriptions.append(QuerySubscription { - $0.intCol > 0 - }) - } - XCTAssertEqual(subscriptions.count, 4) - - subscriptions.write { - subscriptions.remove(ofType: SwiftPerson.self, { - $0.firstName == "Alex" - }) - subscriptions.remove(ofType: SwiftPerson.self, { - $0.firstName == "Belle" - }) - subscriptions.remove(ofType: SwiftTypesSyncObject.self, { - $0.intCol > 0 - }) - } - XCTAssertEqual(subscriptions.count, 1) + try await persons.unsubscribe() + XCTAssertEqual(persons.count, 0) + XCTAssertEqual(persons2.count, 0) + XCTAssertEqual(realm.subscriptions.count, 0) } - func testRemoveSubscription() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append(QuerySubscription(name: "person_names") { - $0.firstName != "Alex" && $0.lastName != "Roy" - }) - subscriptions.append(QuerySubscription { - $0.intCol > 0 - }) + @MainActor + func testFlexibleSyncWithSameType() async throws { + try await populateFlexibleSyncDataForType(SwiftPerson.self) { realm in + for i in 1...21 { + let person = SwiftPerson(firstName: "\(#function)", lastName: "lastname_\(i)", age: i) + realm.add(person) + } } - XCTAssertEqual(subscriptions.count, 2) - let foundSubscription1 = subscriptions.first(named: "person_names") - XCTAssertNotNil(foundSubscription1) - subscriptions.write { - subscriptions.remove(foundSubscription1!) - } + let user = try await self.flexibleSyncApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleSyncApp)) + let realm = try user.realm(configuration: Realm.Configuration()) + let (personsAge15, personsAge10, personsAge5, personsAge0) = try await realm.subscriptions.subscribe(to: QuerySubscription { $0.age > 15 && $0.firstName == "\(#function)" }, QuerySubscription { $0.age > 10 && $0.firstName == "\(#function)" }, QuerySubscription { $0.age > 5 && $0.firstName == "\(#function)" }, QuerySubscription { $0.age >= 0 && $0.firstName == "\(#function)" }) - XCTAssertEqual(subscriptions.count, 1) + XCTAssertEqual(personsAge0.count, 21) + XCTAssertEqual(personsAge5.count, 16) + XCTAssertEqual(personsAge10.count, 11) + XCTAssertEqual(personsAge15.count, 6) + XCTAssertEqual(realm.subscriptions.count, 4) - let foundSubscription2 = subscriptions.first(ofType: SwiftTypesSyncObject.self, where: { - $0.intCol > 0 - }) - XCTAssertNotNil(foundSubscription2) - subscriptions.write { - subscriptions.remove(foundSubscription2!) - } + try await realm.subscriptions.unsubscribeAll(ofType: SwiftPerson.self) - XCTAssertEqual(subscriptions.count, 0) + XCTAssertEqual(personsAge0.count, 0) + XCTAssertEqual(personsAge5.count, 0) + XCTAssertEqual(personsAge10.count, 0) + XCTAssertEqual(personsAge15.count, 0) + XCTAssertEqual(realm.subscriptions.count, 0) } - func testRemoveSubscriptionsByType() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append( - QuerySubscription { - $0.firstName == "Alex" - }, - QuerySubscription { - $0.firstName == "Belle" - }, - QuerySubscription { - $0.firstName == "Charles" - }) - subscriptions.append(QuerySubscription { - $0.intCol > 0 - }) + @MainActor + func testFlexibleSyncUnsubscribeByType() async throws { + try await populateFlexibleSyncDataForType(SwiftPerson.self) { realm in + for i in 1...21 { + let person = SwiftPerson(firstName: "\(#function)", lastName: "lastname_\(i)", age: i) + realm.add(person) + } } - XCTAssertEqual(subscriptions.count, 4) - - subscriptions.write { - subscriptions.removeAll(ofType: SwiftPerson.self) + try await populateFlexibleSyncDataForType(SwiftDog.self) { realm in + for _ in 1...15 { + let dog = SwiftDog(name: "\(#function)", breed: ["bulldog", "poodle", "boxer", "beagle"].randomElement()!) + realm.add(dog) + } } - XCTAssertEqual(subscriptions.count, 1) - - subscriptions.write { - subscriptions.removeAll(ofType: SwiftTypesSyncObject.self) + try await populateFlexibleSyncDataForType(Bird.self) { realm in + for _ in 1...10 { + let bird = Bird(name: "\(#function)", species: [.magpie, .owl, .penguin, .duck].randomElement()!) + realm.add(bird) + } } - XCTAssertEqual(subscriptions.count, 0) - } - func testRemoveAllSubscriptions() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append( - QuerySubscription { - $0.firstName == "Alex" - }, - QuerySubscription { - $0.firstName == "Belle" - }, - QuerySubscription { - $0.firstName == "Charles" - }) - subscriptions.append(QuerySubscription { - $0.intCol > 0 - }) - } - XCTAssertEqual(subscriptions.count, 4) + let user = try await self.flexibleSyncApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleSyncApp)) + let realm = try user.realm(configuration: Realm.Configuration()) + let (personsAge15, personsAge10, personsAge5, personsAge0, dogs, birds) = try await realm.subscriptions.subscribe(to: QuerySubscription { $0.age > 15 && $0.firstName == "\(#function)" }, QuerySubscription { $0.age > 10 && $0.firstName == "\(#function)" }, QuerySubscription { $0.age > 5 && $0.firstName == "\(#function)" }, QuerySubscription { $0.age >= 0 && $0.firstName == "\(#function)" }, QuerySubscription { $0.breed != "labradoodle" && $0.name == "\(#function)" }, QuerySubscription { $0.species.in(BirdSpecies.allCases) && $0.name == "\(#function)" }) - subscriptions.write { - subscriptions.removeAll() - } + XCTAssertEqual(personsAge0.count, 21) + XCTAssertEqual(personsAge5.count, 16) + XCTAssertEqual(personsAge10.count, 11) + XCTAssertEqual(personsAge15.count, 6) + XCTAssertEqual(dogs.count, 15) + XCTAssertEqual(birds.count, 10) + XCTAssertEqual(realm.subscriptions.count, 6) - XCTAssertEqual(subscriptions.count, 0) - } + try await realm.subscriptions.unsubscribeAll(ofType: SwiftPerson.self) - func testSubscriptionSetIterate() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions + XCTAssertEqual(personsAge0.count, 0) + XCTAssertEqual(personsAge5.count, 0) + XCTAssertEqual(personsAge10.count, 0) + XCTAssertEqual(personsAge15.count, 0) + XCTAssertEqual(dogs.count, 15) + XCTAssertEqual(birds.count, 10) + XCTAssertEqual(realm.subscriptions.count, 2) + } - let numberOfSubs = 50 - subscriptions.write { - for i in 1...numberOfSubs { - subscriptions.append(QuerySubscription(name: "person_age_\(i)") { - $0.age > i - }) + @MainActor + func testFlexibleSyncWithDifferentTypes() async throws { + try await populateFlexibleSyncDataForType(SwiftPerson.self) { realm in + for i in 1...21 { + let person = SwiftPerson(firstName: "\(#function)", lastName: "lastname_\(i)", age: i) + realm.add(person) } } - - XCTAssertEqual(subscriptions.count, numberOfSubs) - - var count = 0 - for subscription in subscriptions { - XCTAssertNotNil(subscription) - count += 1 + try await populateFlexibleSyncDataForType(SwiftDog.self) { realm in + for _ in 1...15 { + let dog = SwiftDog(name: "\(#function)", breed: ["bulldog", "poodle", "boxer", "beagle"].randomElement()!) + realm.add(dog) + } } - - XCTAssertEqual(count, numberOfSubs) - } - - func testSubscriptionSetFirstAndLast() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - - let numberOfSubs = 20 - subscriptions.write { - for i in 1...numberOfSubs { - subscriptions.append(QuerySubscription(name: "person_age_\(i)") { - $0.age > i - }) + try await populateFlexibleSyncDataForType(Bird.self) { realm in + for _ in 1...10 { + let bird = Bird(name: "\(#function)", species: [.magpie, .owl, .penguin, .duck].randomElement()!) + realm.add(bird) } } - XCTAssertEqual(subscriptions.count, numberOfSubs) + let user = try await self.flexibleSyncApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleSyncApp)) + let realm = try user.realm(configuration: Realm.Configuration()) + let (persons, dogs, birds) = try await realm.subscriptions.subscribe(to: QuerySubscription { $0.age > 12 && $0.firstName == "\(#function)" }, QuerySubscription { $0.breed != "labradoodle" && $0.name == "\(#function)" }, QuerySubscription { $0.species.in(BirdSpecies.allCases) && $0.name == "\(#function)" }) - let firstSubscription = subscriptions.first - XCTAssertNotNil(firstSubscription!) - XCTAssertEqual(firstSubscription!.name, "person_age_1") - - let lastSubscription = subscriptions.last - XCTAssertNotNil(lastSubscription!) - XCTAssertEqual(lastSubscription!.name, "person_age_\(numberOfSubs)") + XCTAssertEqual(persons.count, 9) + XCTAssertEqual(dogs.count, 15) + XCTAssertEqual(birds.count, 10) + XCTAssertEqual(realm.subscriptions.count, 3) } - func testSubscriptionSetSubscript() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions + @MainActor + func testFlexibleSyncSearchSubscription() async throws { + let user = try await self.flexibleSyncApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleSyncApp)) + let realm = try user.realm(configuration: Realm.Configuration()) + let (_, _, _) = try await realm.subscriptions.subscribe(to: QuerySubscription { $0.age > 12 && $0.firstName == "\(#function)" }, QuerySubscription { $0.breed != "labradoodle" && $0.name == "\(#function)" }, QuerySubscription { $0.species.in(BirdSpecies.allCases) && $0.name == "\(#function)" }) + XCTAssertEqual(realm.subscriptions.count, 3) - let numberOfSubs = 20 - subscriptions.write { - for i in 1...numberOfSubs { - subscriptions.append(QuerySubscription(name: "person_age_\(i)") { - $0.age > i - }) - } - } + let foundedSubscription = realm.subscriptions.first(ofType: SwiftPerson.self, where: { $0.age > 12 && $0.firstName == "\(#function)" }) + XCTAssertNotNil(foundedSubscription) + try await foundedSubscription!.unsubscribe() - XCTAssertEqual(subscriptions.count, numberOfSubs) + XCTAssertEqual(realm.subscriptions.count, 3) - let firstSubscription = subscriptions[0] - XCTAssertNotNil(firstSubscription!) - XCTAssertEqual(firstSubscription!.name, "person_age_1") + try await realm.subscriptions.unsubscribeAll() + XCTAssertEqual(realm.subscriptions.count, 3) - let lastSubscription = subscriptions[numberOfSubs-1] - XCTAssertNotNil(lastSubscription!) - XCTAssertEqual(lastSubscription!.name, "person_age_\(numberOfSubs)") + let notFoundedSubscription = realm.subscriptions.first(ofType: SwiftDog.self, where: { $0.breed != "labradoodle" && $0.name == "\(#function)" }) + XCTAssertNil(notFoundedSubscription) } - func testUpdateQueries() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append( - QuerySubscription(name: "person_age_15") { - $0.age > 15 - }, - QuerySubscription(name: "person_age_20") { - $0.age > 20 - }) - } - XCTAssertEqual(subscriptions.count, 2) - - let foundSubscription1 = subscriptions.first(named: "person_age_15") - let foundSubscription2 = subscriptions.first(named: "person_age_20") - - subscriptions.write { - foundSubscription1?.update(toType: SwiftPerson.self, where: { $0.age > 0 }) - foundSubscription2?.update(toType: SwiftPerson.self, where: { $0.age > 0 }) + @MainActor + func testFlexibleSyncMaxResults() async throws { + try await populateFlexibleSyncDataForType(SwiftPerson.self) { realm in + for i in 1...21 { + let person = SwiftPerson(firstName: "\(#function)", lastName: "lastname_\(i)", age: i) + realm.add(person) + } } - - XCTAssertEqual(subscriptions.count, 2) - } - - func testUpdateQueriesWithoutName() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append( - QuerySubscription { - $0.age > 15 - }, - QuerySubscription { - $0.age > 20 - }) + try await populateFlexibleSyncDataForType(SwiftDog.self) { realm in + for _ in 1...21 { + let dog = SwiftDog(name: "\(#function)", breed: ["bulldog", "poodle", "boxer", "beagle"].randomElement()!) + realm.add(dog) + } } - XCTAssertEqual(subscriptions.count, 2) - - let foundSubscription1 = subscriptions.first(ofType: SwiftPerson.self, where: { - $0.age > 15 - }) - let foundSubscription2 = subscriptions.first(ofType: SwiftPerson.self, where: { - $0.age > 20 - }) - - subscriptions.write { - foundSubscription1?.update(toType: SwiftPerson.self, where: { $0.age > 0 }) - foundSubscription2?.update(toType: SwiftPerson.self, where: { $0.age > 5 }) + try await populateFlexibleSyncDataForType(Bird.self) { realm in + for _ in 1...21 { + let bird = Bird(name: "\(#function)", species: [.magpie, .owl, .penguin, .duck].randomElement()!) + realm.add(bird) + } } - XCTAssertEqual(subscriptions.count, 2) - } + let user = try await self.flexibleSyncApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleSyncApp)) + let realm = try user.realm(configuration: Realm.Configuration()) + let (personsAge15, dogsBulldog, birdsMagpie, personsAge10, dogsPoodle, birdsOwl, personsAge5, dogsBoxer, birdsPenguin, personsAge0, dogsBeagle, birdsDuck) = try await realm.subscriptions.subscribe(to: QuerySubscription { $0.age > 15 && $0.firstName == "\(#function)" }, QuerySubscription { $0.breed == "bulldog" && $0.name == "\(#function)" }, QuerySubscription { $0.species == .magpie && $0.name == "\(#function)" }, QuerySubscription { $0.age > 10 && $0.firstName == "\(#function)" }, QuerySubscription { $0.breed == "poodle" && $0.name == "\(#function)" }, QuerySubscription { $0.species == .owl && $0.name == "\(#function)" }, QuerySubscription { $0.age > 5 && $0.firstName == "\(#function)" }, QuerySubscription { $0.breed == "boxer" && $0.name == "\(#function)" }, QuerySubscription { $0.species == .penguin && $0.name == "\(#function)" }, QuerySubscription { $0.age >= 0 && $0.firstName == "\(#function)" }, QuerySubscription { $0.breed == "beagle" && $0.name == "\(#function)" }, QuerySubscription { $0.species == .duck && $0.name == "\(#function)" }) + XCTAssertEqual(realm.subscriptions.count, 12) - // FIXME: Using `assertThrows` within a Server test will crash on tear down - func skip_testFlexibleSyncAppUpdateQueryWithDifferentObjectTypeWillThrow() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append( - QuerySubscription(name: "person_age_15") { - $0.age > 15 - }) - } - XCTAssertEqual(subscriptions.count, 1) + XCTAssertEqual(personsAge0.count, 21) + XCTAssertEqual(personsAge5.count, 16) + XCTAssertEqual(personsAge10.count, 11) + XCTAssertEqual(personsAge15.count, 6) - let foundSubscription1 = subscriptions.first(named: "person_age_15") + XCTAssertEqual(dogsBulldog.count + dogsPoodle.count + dogsBoxer.count + dogsBeagle.count, 21) + XCTAssertEqual(birdsMagpie.count + birdsOwl.count + birdsPenguin.count + birdsDuck.count, 21) - subscriptions.write { - assertThrows(foundSubscription1?.update(toType: SwiftTypesSyncObject.self, where: { $0.intCol > 0 })) + let newPerson = SwiftPerson(firstName: "\(#function)", lastName: "", age: 8) + try realm.write { + realm.add(newPerson) } - } + XCTAssertEqual(personsAge0.count, 22) + XCTAssertEqual(personsAge5.count, 17) + XCTAssertEqual(personsAge10.count, 11) + XCTAssertEqual(personsAge15.count, 6) - func testFlexibleSyncTransactionsWithPredicateFormatAndNSPredicate() throws { - let realm = try openFlexibleSyncRealm() - let subscriptions = realm.subscriptions - subscriptions.write { - subscriptions.append( - QuerySubscription(name: "name_alex", where: "firstName == %@", "Alex"), - QuerySubscription(name: "name_charles", where: "firstName == %@", "Charles"), - QuerySubscription(where: NSPredicate(format: "firstName == 'Belle'"))) - subscriptions.append(QuerySubscription(where: NSPredicate(format: "intCol > 0"))) + let previousBullDogsCount = dogsBulldog.count + let newDog = SwiftDog(name: "\(#function)", breed: "bulldog") + try realm.write { + realm.add(newDog) } - XCTAssertEqual(subscriptions.count, 4) - let foundSubscription1 = subscriptions.first(ofType: SwiftPerson.self, where: "firstName == %@", "Alex") - XCTAssertNotNil(foundSubscription1) - let foundSubscription2 = subscriptions.first(ofType: SwiftTypesSyncObject.self, where: NSPredicate(format: "intCol > 0")) - XCTAssertNotNil(foundSubscription2) + XCTAssertEqual(previousBullDogsCount + 1, dogsBulldog.count) - subscriptions.write { - subscriptions.remove(ofType: SwiftPerson.self, where: NSPredicate(format: "firstName == 'Belle'")) - subscriptions.remove(ofType: SwiftPerson.self, where: "firstName == %@", "Charles") + try await dogsBulldog.unsubscribe() + XCTAssertEqual(dogsBulldog.count, 0) - foundSubscription1?.update(to: NSPredicate(format: "lastName == 'Wightman'")) - foundSubscription2?.update(to: "stringCol == %@", "string") - } + try await realm.subscriptions.unsubscribeAll() - XCTAssertEqual(subscriptions.count, 2) + XCTAssertEqual(realm.subscriptions.count, 0) + XCTAssertEqual(personsAge0.count, 0) + XCTAssertEqual(personsAge5.count, 0) + XCTAssertEqual(personsAge10.count, 0) + XCTAssertEqual(personsAge15.count, 0) + XCTAssertEqual(dogsBulldog.count, 0) + XCTAssertEqual(dogsPoodle.count, 0) + XCTAssertEqual(dogsBoxer.count, 0) + XCTAssertEqual(dogsBeagle.count, 0) + XCTAssertEqual(birdsMagpie.count, 0) + XCTAssertEqual(birdsOwl.count, 0) + XCTAssertEqual(birdsPenguin.count, 0) + XCTAssertEqual(birdsDuck.count, 0) } -} - -// MARK: - Completion Block -class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { - func testFlexibleSyncAppWithoutQuery() throws { - try populateFlexibleSyncData { realm in - for i in 1...21 { - // Using firstname to query only objects from this test - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - } - - let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) - checkCount(expected: 0, realm, SwiftPerson.self) - - let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) - XCTAssertEqual(subscriptions.count, 0) - waitForDownloads(for: realm) - checkCount(expected: 0, realm, SwiftPerson.self) - } + func testFlexibleSyncQueryThrowsError() async throws { + let user = try await self.flexibleSyncApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleSyncApp)) + let realm = try user.realm() - func testFlexibleSyncAppAddQuery() throws { - try populateFlexibleSyncData { realm in - for i in 1...21 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } + // This throws because the property is not included as a queryable field + do { + let _: QueryResults = try await realm.subscriptions.subscribe(to: { $0.gender == .female }) + XCTFail("Querying on a property which is not included as a queryable field should fail") + } catch { + XCTAssertNotNil(error) } - - let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) - checkCount(expected: 0, realm, SwiftPerson.self) - - let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) - XCTAssertEqual(subscriptions.count, 0) - - let ex = expectation(description: "state change complete") - subscriptions.write({ - subscriptions.append(QuerySubscription(name: "person_age_15") { - $0.age > 15 && $0.firstName == "\(#function)" - }) - }, onComplete: { error in - if error == nil { - ex.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } - }) - - waitForExpectations(timeout: 20.0, handler: nil) - - waitForDownloads(for: realm) - checkCount(expected: 6, realm, SwiftPerson.self) } - func testFlexibleSyncAppMultipleQuery() throws { - try populateFlexibleSyncData { realm in - for i in 1...21 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - let swiftTypes = SwiftTypesSyncObject() - swiftTypes.stringCol = "\(#function)" - realm.add(swiftTypes) - } - - let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) - checkCount(expected: 0, realm, SwiftPerson.self) - - let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) - XCTAssertEqual(subscriptions.count, 0) - - let ex = expectation(description: "state change complete") - subscriptions.write({ - subscriptions.append(QuerySubscription(name: "person_age_10") { - $0.age > 10 && $0.firstName == "\(#function)" - }) - subscriptions.append(QuerySubscription(name: "swift_object_equal_1") { - $0.intCol == 1 && $0.stringCol == "\(#function)" - }) - }, onComplete: { error in - if error == nil { - ex.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } - }) - waitForExpectations(timeout: 20.0, handler: nil) - - waitForDownloads(for: realm) - checkCount(expected: 11, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftTypesSyncObject.self) - } + @MainActor + func testFlexibleSyncSubscriptionSetIterate() async throws { + let user = try await self.flexibleSyncApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleSyncApp)) + let realm = try user.realm(configuration: Realm.Configuration()) - func testFlexibleSyncAppRemoveQuery() throws { - try populateFlexibleSyncData { realm in - for i in 1...21 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - let swiftTypes = SwiftTypesSyncObject() - swiftTypes.stringCol = "\(#function)" - realm.add(swiftTypes) + let numberOfSubs = 25 + for i in 1...numberOfSubs { + let _: QueryResults = try await realm.subscriptions.subscribe(to: { $0.age > i }) } + XCTAssertEqual(realm.subscriptions.count, numberOfSubs) - let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) - checkCount(expected: 0, realm, SwiftPerson.self) - - let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) - XCTAssertEqual(subscriptions.count, 0) - - let ex = expectation(description: "state change complete") - subscriptions.write({ - subscriptions.append(QuerySubscription(name: "person_age_5") { - $0.age > 5 && $0.firstName == "\(#function)" - }) - subscriptions.append(QuerySubscription(name: "swift_object_equal_1") { - $0.intCol == 1 && $0.stringCol == "\(#function)" - }) - }, onComplete: { error in - if error == nil { - ex.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } - }) - waitForExpectations(timeout: 20.0, handler: nil) - - waitForDownloads(for: realm) - checkCount(expected: 16, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftTypesSyncObject.self) - - let ex2 = expectation(description: "state change complete") - subscriptions.write({ - subscriptions.remove(named: "person_age_5") - }, onComplete: { error in - if error == nil { - ex2.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } - }) - waitForExpectations(timeout: 20.0, handler: nil) + var count = 0 + for subscription in realm.subscriptions { + XCTAssertNotNil(subscription) + count += 1 + } - waitForDownloads(for: realm) - checkCount(expected: 0, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftTypesSyncObject.self) + XCTAssertEqual(count, numberOfSubs) } - func testFlexibleSyncAppRemoveAllQueries() throws { - try populateFlexibleSyncData { realm in - for i in 1...21 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - let swiftTypes = SwiftTypesSyncObject() - swiftTypes.stringCol = "\(#function)" - realm.add(swiftTypes) + @MainActor + func testFlexibleSyncSubscriptionSetFirstAndLast() async throws { + let user = try await self.flexibleSyncApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleSyncApp)) + let realm = try user.realm(configuration: Realm.Configuration()) + + let numberOfSubs = 10 + for i in 1...numberOfSubs { + let _: QueryResults = try await realm.subscriptions.subscribe(to: { $0.age > i }) } + XCTAssertEqual(realm.subscriptions.count, numberOfSubs) - let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) - checkCount(expected: 0, realm, SwiftPerson.self) - - let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) - XCTAssertEqual(subscriptions.count, 0) - - let ex = expectation(description: "state change complete") - subscriptions.write({ - subscriptions.append(QuerySubscription(name: "person_age_5") { - $0.age > 5 && $0.firstName == "\(#function)" - }) - subscriptions.append(QuerySubscription(name: "swift_object_equal_1") { - $0.intCol == 1 && $0.stringCol == "\(#function)" - }) - }, onComplete: { error in - if error == nil { - ex.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } - }) - - waitForExpectations(timeout: 20.0, handler: nil) - - waitForDownloads(for: realm) - checkCount(expected: 16, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftTypesSyncObject.self) - - let ex2 = expectation(description: "state change complete") - subscriptions.write({ - subscriptions.removeAll() - subscriptions.append(QuerySubscription(name: "person_age_20") { - $0.age > 20 && $0.firstName == "\(#function)" - }) - }, onComplete: { error in - if error == nil { - ex2.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } - }) - waitForExpectations(timeout: 20.0, handler: nil) + let firstQueryResult = realm.subscriptions.first + XCTAssertTrue((Calendar.current.date(byAdding: DateComponents(hour: -1), to: Date())!...Date()).contains(firstQueryResult!.createdAt)) + XCTAssertEqual(firstQueryResult!.query, "age > 1") - waitForDownloads(for: realm) - checkCount(expected: 1, realm, SwiftPerson.self) - checkCount(expected: 0, realm, SwiftTypesSyncObject.self) + let lastQueryResult = realm.subscriptions.last + XCTAssertTrue((Calendar.current.date(byAdding: DateComponents(hour: -1), to: Date())!...Date()).contains(lastQueryResult!.createdAt)) + XCTAssertEqual(lastQueryResult!.query, "age > 10") } - func testFlexibleSyncAppRemoveQueriesByType() throws { - try populateFlexibleSyncData { realm in - for i in 1...21 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - let swiftTypes = SwiftTypesSyncObject() - swiftTypes.stringCol = "\(#function)" - realm.add(swiftTypes) + @MainActor + func testFlexibleSyncSubscriptionSetSubscript() async throws { + let user = try await self.flexibleSyncApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleSyncApp)) + let realm = try user.realm(configuration: Realm.Configuration()) + + let numberOfSubs = 5 + for i in 1...numberOfSubs { + let _: QueryResults = try await realm.subscriptions.subscribe(to: { $0.age > i }) } + XCTAssertEqual(realm.subscriptions.count, numberOfSubs) - let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) - checkCount(expected: 0, realm, SwiftPerson.self) - - let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) - XCTAssertEqual(subscriptions.count, 0) - - let ex = expectation(description: "state change complete") - subscriptions.write({ - subscriptions.append( - QuerySubscription(name: "person_age_5") { - $0.age > 20 && $0.firstName == "\(#function)" - }, - QuerySubscription(name: "person_age_10") { - $0.lastName == "lastname_1" && $0.firstName == "\(#function)" - }) - subscriptions.append(QuerySubscription(name: "swift_object_equal_1") { - $0.intCol == 1 && $0.stringCol == "\(#function)" - }) - }, onComplete: { error in - if error == nil { - ex.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } - }) - waitForExpectations(timeout: 20.0, handler: nil) - - waitForDownloads(for: realm) - checkCount(expected: 2, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftTypesSyncObject.self) - - let ex2 = expectation(description: "state change complete") - subscriptions.write({ - subscriptions.removeAll(ofType: SwiftPerson.self) - }, onComplete: { error in - if error == nil { - ex2.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } - }) - waitForExpectations(timeout: 20.0, handler: nil) - waitForDownloads(for: realm) - checkCount(expected: 0, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftTypesSyncObject.self) + let firstQueryResult = realm.subscriptions[0] + XCTAssertTrue((Calendar.current.date(byAdding: DateComponents(hour: -1), to: Date())!...Date()).contains(firstQueryResult!.createdAt)) + XCTAssertEqual(firstQueryResult!.query, "age > 1") + + let lastQueryResult = realm.subscriptions[numberOfSubs-1] + XCTAssertTrue((Calendar.current.date(byAdding: DateComponents(hour: -1), to: Date())!...Date()).contains(lastQueryResult!.createdAt)) + XCTAssertEqual(lastQueryResult!.query, "age > 10") } - func testFlexibleSyncAppUpdateQuery() throws { - try populateFlexibleSyncData { realm in + @MainActor + func testFlexibleSyncAnyQueryResultsUnsubscribe() async throws { + try await populateFlexibleSyncDataForType(SwiftPerson.self) { realm in for i in 1...21 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) + let person = SwiftPerson(firstName: "\(#function)", lastName: "lastname_\(i)", age: i) realm.add(person) } } + let user = try await self.flexibleSyncApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleSyncApp)) + let realm = try user.realm(configuration: Realm.Configuration()) - let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) - checkCount(expected: 0, realm, SwiftPerson.self) - - let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) - XCTAssertEqual(subscriptions.count, 0) - - let ex = expectation(description: "state change complete") - subscriptions.write({ - subscriptions.append(QuerySubscription(name: "person_age") { - $0.age > 20 && $0.firstName == "\(#function)" - }) - }, onComplete: { error in - if error == nil { - ex.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } - }) - waitForExpectations(timeout: 20.0, handler: nil) - - waitForDownloads(for: realm) - checkCount(expected: 1, realm, SwiftPerson.self) - - let foundSubscription = subscriptions.first(named: "person_age") - XCTAssertNotNil(foundSubscription) - - let ex2 = expectation(description: "state change complete") - subscriptions.write({ - foundSubscription?.update(toType: SwiftPerson.self, where: { - $0.age > 5 && $0.firstName == "\(#function)" - }) - }, onComplete: { error in - if error == nil { - ex2.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } - }) - waitForExpectations(timeout: 20.0, handler: nil) - - waitForDownloads(for: realm) - checkCount(expected: 16, realm, SwiftPerson.self) - } -} - -// MARK: - Async Await -#if swift(>=5.6) && canImport(_Concurrency) -@available(macOS 12.0.0, *) -class SwiftAsyncFlexibleSyncTests: SwiftSyncTestCase { - override class var defaultTestSuite: XCTestSuite { - // async/await is currently incompatible with thread sanitizer and will - // produce many false positives - // https://bugs.swift.org/browse/SR-15444 - if RLMThreadSanitizerEnabled() { - return XCTestSuite(name: "\(type(of: self))") + let numberOfSubs = 10 + for i in 1...numberOfSubs { + let _: QueryResults = try await realm.subscriptions.subscribe(to: { $0.age > i }) } - return super.defaultTestSuite - } + let _: QueryResults = try await realm.subscriptions.subscribe(to: { $0.breed == "poodle" }) + XCTAssertEqual(realm.subscriptions.count, numberOfSubs+1) - @MainActor - private func populateFlexibleSyncData(_ block: @escaping (Realm) -> Void) async throws { - var config = (try await self.flexibleSyncApp.login(credentials: .anonymous)).flexibleSyncConfiguration() - if config.objectTypes == nil { - config.objectTypes = [SwiftPerson.self, - SwiftTypesSyncObject.self] - } - let realm = try await Realm(configuration: config) - - let subscriptions = realm.subscriptions - try await subscriptions.write { - subscriptions.append(QuerySubscription { - $0.age >= 0 - }) - subscriptions.append(QuerySubscription { - $0.boolCol == true - }) + let subs = realm.subscriptions.filter { $0.query.contains("age") } + for sub in subs { + try await sub.unsubscribe() } - try realm.write { - block(realm) - } + XCTAssertEqual(realm.subscriptions.count, 1) } @MainActor - func testFlexibleSyncAppAddQueryAsyncAwait() async throws { - try await populateFlexibleSyncData { realm in + func testFlexibleSyncAnyQueryResultsCast() async throws { + try await populateFlexibleSyncDataForType(SwiftPerson.self) { realm in for i in 1...21 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) + let person = SwiftPerson(firstName: "\(#function)", lastName: "lastname_\(i)", age: i) realm.add(person) } } + let user = try await self.flexibleSyncApp.login(credentials: basicCredentials(usernameSuffix: "", app: flexibleSyncApp)) + let realm = try user.realm(configuration: Realm.Configuration()) - var config = try await self.flexibleSyncApp.login(credentials: basicCredentials(app: self.flexibleSyncApp)).flexibleSyncConfiguration() - config.objectTypes = [SwiftPerson.self, SwiftTypesSyncObject.self] - let realm = try await Realm(configuration: config) - XCTAssertNotNil(realm) - checkCount(expected: 0, realm, SwiftPerson.self) - - let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) - XCTAssertEqual(subscriptions.count, 0) + let originalPersons: QueryResults = try await realm.subscriptions.subscribe(to: { $0.age > 15 }) + XCTAssertEqual(realm.subscriptions.count, 1) + XCTAssertEqual(originalPersons.count, 6) - try await subscriptions.write { - subscriptions.append(QuerySubscription(name: "person_age_15") { - $0.age > 15 && $0.firstName == "\(#function)" - }) - } + let anyQueryResults = realm.subscriptions[0] + XCTAssertTrue((Calendar.current.date(byAdding: DateComponents(hour: -1), to: Date())!...Date()).contains(anyQueryResults!.createdAt)) + XCTAssertEqual(anyQueryResults!.query, "age > 15") - checkCount(expected: 6, realm, SwiftPerson.self) - } + let persons = anyQueryResults?.as(type: SwiftPerson.self) + XCTAssertNotNil(persons) + XCTAssertEqual(persons!.count, 6) - @MainActor - func testStates() async throws { - var config = (try await self.flexibleSyncApp.login(credentials: .anonymous)) - .flexibleSyncConfiguration() - config.objectTypes = [SwiftPerson.self, SwiftTypesSyncObject.self] - let realm = try await Realm(configuration: config) - XCTAssertNotNil(realm) - - let subscriptions = realm.subscriptions - XCTAssertEqual(subscriptions.count, 0) - - // should complete - try await subscriptions.write { - subscriptions.append(QuerySubscription(name: "person_age_15") { - $0.age > 15 && $0.firstName == "\(#function)" - }) - } - XCTAssertEqual(subscriptions.state, .complete) - // should error - do { - try await subscriptions.write { - subscriptions.append(QuerySubscription(name: "swiftObject_longCol") { - $0.longCol == Int64(1) - }) - } - XCTFail("Invalid query should have failed") - } catch let error { - if let error = error as NSError? { - XCTAssertTrue(error.domain == RLMFlexibleSyncErrorDomain) - XCTAssertTrue(error.code == 2) - } + let notPersons = anyQueryResults?.as(type: SwiftDog.self) + XCTAssertNil(notPersons) - guard case .error = subscriptions.state else { - return XCTFail("Adding a query for a not queryable field should change the subscription set state to error") - } - } + try await anyQueryResults!.unsubscribe() + XCTAssertEqual(originalPersons.count, 6) + XCTAssertEqual(persons!.count, 6) } } diff --git a/Realm/ObjectServerTests/SwiftObjectServerTests.swift b/Realm/ObjectServerTests/SwiftObjectServerTests.swift index 99f27f643bb..fabba91e678 100644 --- a/Realm/ObjectServerTests/SwiftObjectServerTests.swift +++ b/Realm/ObjectServerTests/SwiftObjectServerTests.swift @@ -2813,7 +2813,6 @@ class CombineObjectServerTests: SwiftSyncTestCase { } #if swift(>=5.6) && canImport(_Concurrency) - @available(macOS 12.0, *) class AsyncAwaitObjectServerTests: SwiftSyncTestCase { override class var defaultTestSuite: XCTestSuite { diff --git a/Realm/ObjectServerTests/SwiftServerObjects.swift b/Realm/ObjectServerTests/SwiftServerObjects.swift index 7e88c55fc72..743fdb0f2be 100644 --- a/Realm/ObjectServerTests/SwiftServerObjects.swift +++ b/Realm/ObjectServerTests/SwiftServerObjects.swift @@ -36,6 +36,43 @@ public class SwiftPerson: Object { @available(OSX 10.15, watchOS 6.0, iOS 13.0, iOSApplicationExtension 13.0, OSXApplicationExtension 10.15, tvOS 13.0, *) extension SwiftPerson: ObjectKeyIdentifiable {} +public class SwiftDog: Object { + public enum Gender: Int, PersistableEnum { + case male + case female + case unknown + } + @Persisted(primaryKey: true) public var _id: ObjectId = ObjectId.generate() + @Persisted var name: String + @Persisted var breed: String + @Persisted var gender: Gender + + public convenience init(name: String, breed: String, gender: Gender? = .unknown) { + self.init() + self.name = name + self.breed = breed + } +} + +public enum BirdSpecies: Int, PersistableEnum { + case magpie + case owl + case penguin + case duck +} + +public class Bird: Object { + @Persisted(primaryKey: true) public var _id: ObjectId = ObjectId.generate() + @Persisted var name: String + @Persisted var species: BirdSpecies + + public convenience init(name: String, species: BirdSpecies) { + self.init() + self.name = name + self.species = species + } +} + public class SwiftTypesSyncObject: Object { @Persisted(primaryKey: true) public var _id: ObjectId = ObjectId.generate() @Persisted public var boolCol: Bool = true diff --git a/Realm/ObjectServerTests/SwiftSyncTestCase.swift b/Realm/ObjectServerTests/SwiftSyncTestCase.swift index 41ad470d668..817d54ab0b0 100644 --- a/Realm/ObjectServerTests/SwiftSyncTestCase.swift +++ b/Realm/ObjectServerTests/SwiftSyncTestCase.swift @@ -199,61 +199,6 @@ open class SwiftSyncTestCase: RLMSyncTestCase { XCTFail("Got an error: \(error) (process: \(isParent ? "parent" : "child"))") } } - - // MARK: - Flexible Sync Use Cases - - public func openFlexibleSyncRealmForUser(_ user: User) throws -> Realm { - var config = user.flexibleSyncConfiguration() - if config.objectTypes == nil { - config.objectTypes = [SwiftPerson.self, - SwiftTypesSyncObject.self] - } - let realm = try Realm(configuration: config) - waitForDownloads(for: realm) - return realm - } - - public func openFlexibleSyncRealm() throws -> Realm { - let user = try logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - var config = user.flexibleSyncConfiguration() - if config.objectTypes == nil { - config.objectTypes = [SwiftPerson.self, - SwiftTypesSyncObject.self] - } - return try Realm(configuration: config) - } - - public func flexibleSyncRealm() throws -> Realm { - let user = try logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - return try openFlexibleSyncRealmForUser(user) - } - - public func populateFlexibleSyncData(_ block: @escaping (Realm) -> Void) throws { - try writeToFlxRealm { realm in - try realm.write { - block(realm) - } - self.waitForUploads(for: realm) - } - } - - public func writeToFlxRealm(_ block: @escaping (Realm) throws -> Void) throws { - let realm = try flexibleSyncRealm() - let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) - let ex = expectation(description: "state change complete") - subscriptions.write({ - subscriptions.append(QuerySubscription(where: "TRUEPREDICATE")) - subscriptions.append(QuerySubscription(where: "TRUEPREDICATE")) - }, onComplete: { error in - XCTAssertNil(error) - ex.fulfill() - }) - XCTAssertEqual(subscriptions.count, 2) - - waitForExpectations(timeout: 20.0, handler: nil) - try block(realm) - } } #if swift(>=5.6) && canImport(_Concurrency) diff --git a/Realm/RLMSyncSubscription.mm b/Realm/RLMSyncSubscription.mm index c8c4c18bd58..ab4eafee5f6 100644 --- a/Realm/RLMSyncSubscription.mm +++ b/Realm/RLMSyncSubscription.mm @@ -70,6 +70,18 @@ - (NSString *)objectClassName { return RLMStringViewToNSString(_subscription->object_class_name()); } +- (void)unsubscribeOnComplete:(void(^)(NSError *))completionBlock { + [_subscriptionSet write:^{ + [_subscriptionSet removeSubscription:self]; + } onComplete:^(NSError* error) { + if (error == nil) { + completionBlock(nil); + } else { + completionBlock(error); + } + }]; +} + - (void)updateSubscriptionWhere:(NSString *)predicateFormat, ... { va_list args; va_start(args, predicateFormat); diff --git a/Realm/RLMSyncSubscription_Private.h b/Realm/RLMSyncSubscription_Private.h index 41e866bf9ce..576d07d51c3 100644 --- a/Realm/RLMSyncSubscription_Private.h +++ b/Realm/RLMSyncSubscription_Private.h @@ -32,6 +32,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSString *objectClassName; +- (void)unsubscribeOnComplete:(void(^)(NSError *))completionBlock; + @end #pragma mark - SubscriptionSet diff --git a/Realm/Tests/SwiftUISyncTestHost/ContentView.swift b/Realm/Tests/SwiftUISyncTestHost/ContentView.swift index 5f71539b654..6214d6106e2 100644 --- a/Realm/Tests/SwiftUISyncTestHost/ContentView.swift +++ b/Realm/Tests/SwiftUISyncTestHost/ContentView.swift @@ -25,6 +25,7 @@ enum LoggingViewState { case loggingIn case loggedIn case syncing + case flexibleSync } struct MainView: View { @@ -65,6 +66,10 @@ struct MainView: View { viewState = .syncing } .accessibilityIdentifier("sync_button") + Button("Flexible Sync") { + viewState = .flexibleSync + } + .accessibilityIdentifier("flexible_sync_button") } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.yellow) @@ -108,6 +113,25 @@ struct MainView: View { default: EmptyView() } + case .flexibleSync: + if #available(macOS 12.0, *) { + switch testType { + case "flexible_sync_observed_query_results_state": + ObservedQueryResultsStateView() + .environment(\.realmConfiguration, user!.flexibleSyncConfiguration()) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.green) + .transition(AnyTransition.move(edge: .leading)).animation(.default) + case "flexible_sync_observed_query_results": + ObservedQueryResultsView() + .environment(\.realmConfiguration, user!.flexibleSyncConfiguration()) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.green) + .transition(AnyTransition.move(edge: .leading)).animation(.default) + default: + EmptyView() + } + } else {} } } } @@ -138,6 +162,15 @@ struct LoginView: View { }) } .accessibilityIdentifier("login_button_2") + Button("Log In Anonymous") { + loggingIn() + loginHelper.login(email: nil, + password: nil, + completion: { user in + didLogin(user) + }) + } + .accessibilityIdentifier("login_button_3") Button("Logout") { loginHelper.logout() } @@ -162,9 +195,10 @@ class LoginHelper: ObservableObject { return applicationSupportDirectory.appendingPathComponent(Bundle.main.bundleIdentifier!) } - func login(email: String, password: String, completion: @escaping (User) -> Void) { + func login(email: String? = nil, password: String? = nil, completion: @escaping (User) -> Void) { let app = RealmSwift.App(id: ProcessInfo.processInfo.environment["app_id"]!, configuration: appConfig, rootDirectory: clientDataRoot) - app.login(credentials: .emailPassword(email: email, password: password)) + let credentials: Credentials = (email != nil) ? .emailPassword(email: email!, password: password!) : .anonymous + app.login(credentials: credentials) .receive(on: DispatchQueue.main) .sink(receiveCompletion: { result in if case let .failure(error) = result { @@ -349,6 +383,74 @@ struct AutoOpenPartitionView: View { } } +@available(macOS 12.0, *) +struct ObservedQueryResultsStateView: View { + @ObservedQueryResults(SwiftPerson.self, + subscription: { $0.age > 18 && $0.firstName == ProcessInfo.processInfo.environment["firstName"]! }) + var persons + + var body: some View { + VStack { + switch $persons.state { + case .pending: + ProgressView() + case .completed: + List { + ForEach(persons) { person in + Text("\(person.firstName)") + } + } + case .error(let error): + ErrorView(error: error) + .background(Color.red) + .transition(AnyTransition.move(edge: .trailing)).animation(.default) + } + Spacer() + Button("Unsubscribe") { + Task { + do { + try await $persons.unsubscribe() + } + } + } + .accessibilityIdentifier("unsubscribe_button") + } + } +} + +@available(macOS 12.0, *) +struct ObservedQueryResultsView: View { + @ObservedQueryResults(SwiftPerson.self, + subscription: { $0.age >= 15 && $0.firstName == ProcessInfo.processInfo.environment["firstName"]! }) + var persons + @State var searchFilter: String = "" + + var body: some View { + VStack { + if persons.isEmpty { + ProgressView() + } else { + List { + ForEach(persons) { person in + HStack { + Text("\(person.firstName)") + Spacer() + Text("\(person.age)") + } + } + } + } + } + .onDisappear { + Task { + do { + try await $persons.unsubscribe() + } + } + } + } +} + struct ErrorView: View { var error: Error var body: some View { @@ -369,6 +471,6 @@ struct ListView: View { Text("\(object.firstName)") } } - .navigationTitle("SwiftHugeSyncObject's List") + .navigationTitle("SwiftPerson's List") } } diff --git a/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift b/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift index 9ef59f0af62..7a53956c227 100644 --- a/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift +++ b/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift @@ -15,13 +15,14 @@ // limitations under the License. // //////////////////////////////////////////////////////////////////////////// - import XCTest import RealmSwift +import Realm -class SwiftUISyncTestHostUITests: XCTestCase { +class SwiftUISyncTestCases: XCTestCase { // Create App only once static var appId: String? + static var flexibleSyncAppId: String? // App Runner Directory var clientDataRoot: URL { @@ -35,18 +36,24 @@ class SwiftUISyncTestHostUITests: XCTestCase { } // App Info - private var appId: String? { + fileprivate var appId: String? { SwiftUISyncTestHostUITests.appId } - private var app: App? + fileprivate var app: App? + + // Flexible Sync + fileprivate var flexibleSyncAppId: String? { + SwiftUISyncTestHostUITests.flexibleSyncAppId + } + fileprivate var flexibleSyncApp: App? // User Info - private var username1 = "" - private var username2 = "" - private let password = "password" + fileprivate var username1 = "" + fileprivate var username2 = "" + fileprivate let password = "password" // Application - private let application = XCUIApplication() + fileprivate let application = XCUIApplication() // MARK: - Test Lifecycle override class func setUp() { @@ -56,31 +63,6 @@ class SwiftUISyncTestHostUITests: XCTestCase { } } - override func setUp() { - super.setUp() - continueAfterFailure = false - - try? FileManager.default.createDirectory(at: clientDataRoot, withIntermediateDirectories: true) - try? FileManager.default.createDirectory(at: appClientDataRoot, withIntermediateDirectories: true) - - // Create App once for this Test Suite - if SwiftUISyncTestHostUITests.appId == nil { - do { - let appId = try RealmServer.shared.createApp() - SwiftUISyncTestHostUITests.appId = appId - } catch { - XCTFail("Cannot initialise test without a creating an App on the server") - } - } - - // Instantiate App from appId after - do { - app = try getApp() - } catch { - print("Error creating user \(error)") - } - } - override func tearDown() { logoutAllUsers() application.terminate() @@ -97,21 +79,6 @@ class SwiftUISyncTestHostUITests: XCTestCase { } super.tearDown() } -} - -// MARK: - -extension SwiftUISyncTestHostUITests { - private func getApp() throws -> App { - super.setUp() - // Setup App for Testing - let appConfiguration = RLMAppConfiguration(baseURL: "http://localhost:9090", - transport: nil, - localAppName: nil, - localAppVersion: nil, - defaultRequestTimeoutMS: 60) - // Create app in current process - return App(id: appId!, configuration: appConfiguration, rootDirectory: clientDataRoot) - } private func resetSyncManager() { guard appId != nil, let app = app else { @@ -142,39 +109,29 @@ extension SwiftUISyncTestHostUITests { wait(for: exArray, timeout: 60.0) } } +} - private func createUsers(email: String, password: String, n: Int) throws -> User { - let user = try registerAndLoginUser(email: email, password: password) - let config = user.configuration(partitionValue: user.id) - let realm = try openRealm(configuration: config, for: user) - try realm.write { - (1...n).forEach { _ in - realm.add(SwiftPerson(firstName: randomString(7), lastName: randomString(7))) - } - } - user.waitForUpload(toFinish: user.id) - return user - } - - private func registerAndLoginUser(email: String, password: String) throws -> User { - try registerUser(email: email, password: password) - return try loginUser(email: email, password: password) +// MARK: - +extension SwiftUISyncTestCases { + fileprivate func registerAndLoginUser(email: String, password: String, for app: App) throws -> User { + try registerUser(email: email, password: password, for: app) + return try loginUser(email: email, password: password, for: app) } - private func registerUser(email: String, password: String) throws { + fileprivate func registerUser(email: String, password: String, for app: App) throws { let ex = expectation(description: "Should register in the user properly") - app!.emailPasswordAuth.registerUser(email: email, password: password, completion: { error in + app.emailPasswordAuth.registerUser(email: email, password: password, completion: { error in XCTAssertNil(error) ex.fulfill() }) waitForExpectations(timeout: 4, handler: nil) } - private func loginUser(email: String, password: String) throws -> User { + fileprivate func loginUser(email: String, password: String, for app: App) throws -> User { var syncUser: User! let ex = expectation(description: "Should log in the user properly") let credentials = Credentials.emailPassword(email: email, password: password) - app!.login(credentials: credentials) { result in + app.login(credentials: credentials) { result in switch result { case .success(let user): syncUser = user @@ -189,7 +146,7 @@ extension SwiftUISyncTestHostUITests { return syncUser } - private func openRealm(configuration: Realm.Configuration, for user: User) throws -> Realm { + fileprivate func openRealm(configuration: Realm.Configuration, for user: User) throws -> Realm { var configuration = configuration if configuration.objectTypes == nil { configuration.objectTypes = [SwiftPerson.self] @@ -200,11 +157,13 @@ extension SwiftUISyncTestHostUITests { } // Login for given email and password - enum UserType: Int { + fileprivate enum UserType: Int { case first = 1 case second = 2 + case anonymous = 3 + } - private func loginUser(_ type: UserType) { + fileprivate func loginUser(_ type: UserType) { let loginButton = application.buttons["login_button_\(type.rawValue)"] XCTAssertTrue(loginButton.waitForExistence(timeout: 2)) loginButton.tap() @@ -213,7 +172,7 @@ extension SwiftUISyncTestHostUITests { XCTAssertTrue(loggingView.waitForExistence(timeout: 2)) } - private func asyncOpen() { + fileprivate func loginAndAsync() { loginUser(.first) // Query for button to start syncing @@ -222,18 +181,69 @@ extension SwiftUISyncTestHostUITests { syncButtonView.tap() } - func logoutAllUsers() { + fileprivate func logoutAllUsers() { let loginButton = application.buttons["logout_users_button"] XCTAssertTrue(loginButton.waitForExistence(timeout: 2)) loginButton.tap() } - public func randomString(_ length: Int) -> String { + fileprivate func randomString(_ length: Int) -> String { let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" return String((0.. App { + // Setup App for Testing + let appConfiguration = RLMAppConfiguration(baseURL: "http://localhost:9090", + transport: nil, + localAppName: nil, + localAppVersion: nil, + defaultRequestTimeoutMS: 60) + // Create app in current process + return App(id: appId!, configuration: appConfiguration, rootDirectory: clientDataRoot) + } + + private func createUsers(email: String, password: String, n: Int) throws -> User { + let user = try registerAndLoginUser(email: email, password: password, for: app!) + let config = user.configuration(partitionValue: user.id) + let realm = try openRealm(configuration: config, for: user) + try realm.write { + (1...n).forEach { _ in + realm.add(SwiftPerson(firstName: randomString(7), lastName: randomString(7))) + } + } + user.waitForUpload(toFinish: user.id) + return user + } +} + // MARK: - AsyncOpen extension SwiftUISyncTestHostUITests { func testDownloadRealmAsyncOpenApp() throws { @@ -247,7 +257,7 @@ extension SwiftUISyncTestHostUITests { application.launchEnvironment["partition_value"] = user.id application.launch() - asyncOpen() + loginAndAsync() // Test progress is greater than 0 let progressView = application.staticTexts["progress_text_view"] @@ -275,7 +285,7 @@ extension SwiftUISyncTestHostUITests { application.launchEnvironment["app_id"] = appId application.launch() - asyncOpen() + loginAndAsync() // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -293,7 +303,7 @@ extension SwiftUISyncTestHostUITests { application.launchEnvironment["app_id"] = appId application.launch() - asyncOpen() + loginAndAsync() // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -308,14 +318,14 @@ extension SwiftUISyncTestHostUITests { let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" let email2 = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let user1 = try registerAndLoginUser(email: email, password: password) - let user2 = try registerAndLoginUser(email: email2, password: password) + let user1 = try registerAndLoginUser(email: email, password: password, for: app!) + let user2 = try registerAndLoginUser(email: email2, password: password, for: app!) let config1 = user1.configuration(partitionValue: partitionValue) let config2 = user2.configuration(partitionValue: partitionValue) let realm = try Realm(configuration: config1) - try! realm.write { + try realm.write { realm.add(SwiftPerson(firstName: "Joe", lastName: "Blogs")) realm.add(SwiftPerson(firstName: "Jane", lastName: "Doe")) } @@ -329,7 +339,7 @@ extension SwiftUISyncTestHostUITests { application.launchEnvironment["app_id"] = appId application.launch() - asyncOpen() + loginAndAsync() // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -378,7 +388,7 @@ extension SwiftUISyncTestHostUITests { application.launchEnvironment["app_id"] = appId application.launch() - asyncOpen() + loginAndAsync() // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -407,7 +417,7 @@ extension SwiftUISyncTestHostUITests { application.launchEnvironment["app_id"] = appId application.launch() - asyncOpen() + loginAndAsync() // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -437,7 +447,7 @@ extension SwiftUISyncTestHostUITests { application.launch() // Test that the user is already logged in - asyncOpen() + loginAndAsync() // Test progress is greater than 0 let progressView = application.staticTexts["progress_text_view"] @@ -465,7 +475,7 @@ extension SwiftUISyncTestHostUITests { application.launchEnvironment["app_id"] = appId application.launch() - asyncOpen() + loginAndAsync() // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -483,7 +493,7 @@ extension SwiftUISyncTestHostUITests { application.launchEnvironment["app_id"] = appId application.launch() - asyncOpen() + loginAndAsync() // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -504,7 +514,7 @@ extension SwiftUISyncTestHostUITests { application.launchEnvironment["app_id"] = appId application.launch() - asyncOpen() + loginAndAsync() // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -533,7 +543,7 @@ extension SwiftUISyncTestHostUITests { application.launchEnvironment["app_id"] = appId application.launch() - asyncOpen() + loginAndAsync() // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -548,3 +558,150 @@ extension SwiftUISyncTestHostUITests { XCTAssertTrue(waitingUserView.waitForExistence(timeout: 2)) } } + +@available(macOS 12.0.0, *) +class SwiftUIFlexibleSyncTestHostUITests: SwiftUISyncTestCases { + override func setUp() { + continueAfterFailure = false + + try? FileManager.default.createDirectory(at: clientDataRoot, withIntermediateDirectories: true) + try? FileManager.default.createDirectory(at: appClientDataRoot, withIntermediateDirectories: true) + + // Create App once for this Test Suite + if SwiftUISyncTestHostUITests.flexibleSyncAppId == nil { + do { + let appId = try RealmServer.shared.createAppWithQueryableFields(["age", "firstName"]) + SwiftUISyncTestHostUITests.flexibleSyncAppId = appId + } catch { + XCTFail("Cannot initialise test without a creating an App on the server") + } + } + + // Instantiate App from appId after + do { + flexibleSyncApp = try getApp() + } catch { + print("Error creating user \(error)") + } + } + + private func getApp() throws -> App { + // Setup App for Testing + let appConfiguration = RLMAppConfiguration(baseURL: "http://localhost:9090", + transport: nil, + localAppName: nil, + localAppVersion: nil, + defaultRequestTimeoutMS: 60) + // Create app in current process + return App(id: flexibleSyncAppId!, configuration: appConfiguration, rootDirectory: clientDataRoot) + } + + @MainActor + private func populateFlexibleSyncData(_ block: @escaping (Realm) -> Void) async throws { + let user = try await self.flexibleSyncApp!.login(credentials: .anonymous) + let realm = try user.realm(configuration: Realm.Configuration()) + let _: QueryResults = try await realm.subscriptions.subscribe { $0.age >= 0 } + try realm.write { + block(realm) + } + try user.waitForUploads(in: realm) + } + + func testObservedQueryResultsState() async throws { + try await populateFlexibleSyncData { realm in + for i in 1...21 { + let person = SwiftPerson(firstName: "\(#function)", + lastName: "lastname_\(i)", + age: i) + realm.add(person) + } + } + + application.launchEnvironment["async_view_type"] = "flexible_sync_observed_query_results_state" + application.launchEnvironment["app_id"] = flexibleSyncAppId + application.launchEnvironment["firstName"] = "\(#function)" + application.launch() + + loginUser(.anonymous) + + // Query for button to start syncing + let syncButtonView = application.buttons["flexible_sync_button"] + XCTAssertTrue(syncButtonView.waitForExistence(timeout: 2)) + syncButtonView.tap() + + // Test show ListView after syncing realm + let table = application.tables.firstMatch + XCTAssertTrue(table.waitForExistence(timeout: 6)) + XCTAssertEqual(table.cells.count, 3) + + try await populateFlexibleSyncData { realm in + for i in 22...30 { + let person = SwiftPerson(firstName: "\(#function)", + lastName: "lastname_\(i)", + age: i) + realm.add(person) + } + } + + XCTAssertTrue(table.waitForExistence(timeout: 6)) + XCTAssertEqual(table.cells.count, 12) + + // Query for button to unsubscribe from query + let unsubscribeButtonView = application.buttons["unsubscribe_button"] + XCTAssertTrue(unsubscribeButtonView.waitForExistence(timeout: 2)) + unsubscribeButtonView.tap() + + XCTAssertTrue(table.waitForExistence(timeout: 6)) + XCTAssertEqual(table.cells.count, 0) + } + + func testObservedQueryResults() async throws { + try await populateFlexibleSyncData { realm in + for i in 1...21 { + let person = SwiftPerson(firstName: "\(#function)", + lastName: "lastname_\(i)", + age: i) + realm.add(person) + } + } + + application.launchEnvironment["async_view_type"] = "flexible_sync_observed_query_results" + application.launchEnvironment["app_id"] = flexibleSyncAppId + application.launchEnvironment["firstName"] = "\(#function)" + application.launch() + + loginUser(.anonymous) + + // Query for button to start syncing + let syncButtonView = application.buttons["flexible_sync_button"] + XCTAssertTrue(syncButtonView.waitForExistence(timeout: 2)) + syncButtonView.tap() + + // Test show ListView after syncing realm + let table = application.tables.firstMatch + XCTAssertTrue(table.waitForExistence(timeout: 6)) + XCTAssertEqual(table.cells.count, 7) + + try await populateFlexibleSyncData { realm in + for i in 22...30 { + let person = SwiftPerson(firstName: "\(#function)", + lastName: "lastname_\(i)", + age: i) + realm.add(person) + } + } + + XCTAssertTrue(table.waitForExistence(timeout: 6)) + XCTAssertEqual(table.cells.count, 16) + } +} + +extension User { + func waitForUploads(in realm: Realm) throws { + try waitForUploads(for: ObjectiveCSupport.convert(object: realm)) + } + + func waitForDownloads(in realm: Realm) throws { + try waitForDownloads(for: ObjectiveCSupport.convert(object: realm)) + } +} diff --git a/RealmSwift/Realm.swift b/RealmSwift/Realm.swift index 553f3b5356d..630c37c68fd 100644 --- a/RealmSwift/Realm.swift +++ b/RealmSwift/Realm.swift @@ -989,7 +989,7 @@ extension Realm { */ @available(*, message: "This feature is currently in beta.") public var subscriptions: SyncSubscriptionSet { - return SyncSubscriptionSet(rlmRealm.subscriptions) + return SyncSubscriptionSet(rlmRealm.subscriptions, realm: self) } } diff --git a/RealmSwift/SwiftUI.swift b/RealmSwift/SwiftUI.swift index 331e836fcf7..c6a7ef09d60 100644 --- a/RealmSwift/SwiftUI.swift +++ b/RealmSwift/SwiftUI.swift @@ -617,6 +617,302 @@ extension Projection: _ObservedResultsValue { } } } +public enum ObservedQueryResultsState { + /// Subscription has been added and waiting for data to bootstrap. + case pending + /// An error has occurred while adding the subscription (client or server side). + case error(Error) + /// Data has been bootstrapped and query results updated. + case completed +} + +// MARK: ObservedQueryResults + +/// A property wrapper type that represents the results of a query on a realm resulting from a query subscription. +/// +/// The results use the realm configuration provided by +/// the environment value `EnvironmentValues/realmConfiguration`. or the configuration injected on the initializer. +/// +/// `ObservedQueryResults` is mutable, you can add or remove elements from the results, +/// if this object are out of the view of the subscription query this will not be included on this results +/// +#if swift(>=5.6) && canImport(_Concurrency) +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +@propertyWrapper public struct ObservedQueryResults: DynamicProperty, BoundCollection where ResultType: _ObservedResultsValue & RealmFetchable & KeypathSortable & Identifiable { + private class Storage: ObservableStorage> { + var setupHasRun = false + private let subscription: ((Query) -> Query)? + var queryResults: QueryResults? + + private func didSet() { + if setupHasRun { + setupValue() + } + } + + func setupValue() { + guard let queryResults = queryResults, + let results = queryResults.results else { + return + } + + value = results + if let sortDescriptor = sortDescriptor { + value = value.sorted(byKeyPath: sortDescriptor.keyPath, ascending: sortDescriptor.ascending) + } + + let filters = [searchFilter, filter ?? `where`].compactMap { $0 } + if !filters.isEmpty { + let compoundFilter = NSCompoundPredicate(andPredicateWithSubpredicates: filters) + value = value.filter(compoundFilter) + } + } + + var sortDescriptor: SortDescriptor? { + didSet { + didSet() + } + } + + var filter: NSPredicate? { + didSet { + didSet() + } + } + var `where`: NSPredicate? { + didSet { + didSet() + } + } + var configuration: Realm.Configuration? { + didSet { + didSet() + } + } + + var searchString: String = "" + var searchFilter: NSPredicate? { + didSet { + didSet() + } + } + + func subscribe() { + guard let configuration = configuration else { + throwRealmException("") + } + + guard let syncConfiguration = configuration.syncConfiguration, + syncConfiguration.isFlexibleSync else { + throwRealmException("") + } + + subscribe(configuration: configuration) + setupHasRun = true + } + + private func subscribe(configuration: Realm.Configuration) { + Task.detached { + do { + let realm = try await Realm(configuration: configuration) + let results: QueryResults + if let subscription = self.subscription { + results = try await realm.subscriptions.subscribe(to: subscription) + } else { + results = try await realm.subscriptions.subscribe(to: ResultType.self) + } + self.queryResults = results + self.setupValue() + self.state = .completed + } catch { + self.state = .error(error) + } + } + } + + @Published var state: ObservedQueryResultsState = .pending { + willSet { + objectWillChange.send() + } + } + + init(_ results: Results, + _ subscription: ((Query) -> Query)? = nil, + _ keyPaths: [String]? = nil) { + self.subscription = subscription + super.init(results, keyPaths) + } + } + + @Environment(\.realmConfiguration) var configuration + @ObservedObject private var storage: Storage + /// :nodoc: + fileprivate func searchText(_ text: String, on keyPath: KeyPath) { + if text.isEmpty { + if storage.searchFilter != nil { + storage.searchFilter = nil + } + } else if text != storage.searchString { + storage.searchFilter = Query()[dynamicMember: keyPath].contains(text).predicate + } + storage.searchString = text + } + /// Stores an NSPredicate used for filtering the Results. This is mutually exclusive + /// to the `where` parameter. + @State public var filter: NSPredicate? { + willSet { + storage.where = nil + storage.filter = newValue + } + } +#if swift(>=5.5) + /// Stores a type safe query used for filtering the Results. This is mutually exclusive + /// to the `filter` parameter. + @State public var `where`: ((Query) -> Query)? { + // The introduction of this property produces a compiler bug in + // Xcode 12.5.1. So Swift Queries are supported on Xcode 13 and above + // when used with SwiftUI. + willSet { + storage.filter = nil + storage.where = newValue?(Query()).predicate + } + } +#endif + /// :nodoc: + @State public var sortDescriptor: SortDescriptor? { + willSet { + storage.sortDescriptor = newValue + } + } + + /// :Returns the current state for the subscription, if used this will update in case the state changes. + public var state: ObservedQueryResultsState { + return storage.state + } + + /// Unsubscribe the current `QueryResults`, associated to this property wrapper, + /// also will remove the data associated to that subscription from the results. + public func unsubscribe() async throws { + guard let queryResults = storage.queryResults else { + return + } + try await queryResults.unsubscribe() + } + + /// :nodoc: + public var wrappedValue: Results { + if !storage.setupHasRun { + storage.subscribe() + } + return storage.queryResults != nil ? storage.value.freeze() : storage.value + } + /// :nodoc: + public var projectedValue: Self { + if !storage.setupHasRun { + storage.subscribe() + } + return self + } + + /** + Initialize a `ObservedQueryResults` struct for a given `Projection` type. + - parameter type: Observed type + - parameter subscription: The query used for the subscription. + - 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 configuration is set to the `defaultConfiguration` + - parameter filter: Observations will be made only for passing objects. + If no filter given - all objects will be observed + - parameter keyPaths: Only properties contained in the key paths array will be observed. + If `nil`, notifications will be delivered for any property change on the object. + String key paths which do not correspond to a valid a property will throw an exception. + - parameter sortDescriptor: A sequence of `SortDescriptor`s to sort by + */ + public init(_ type: ResultType.Type, + subscription: ((Query) -> Query)? = nil, + configuration: Realm.Configuration? = nil, + filter: NSPredicate? = nil, + keyPaths: [String]? = nil, + sortDescriptor: SortDescriptor? = nil) where ResultType: Projection, ObjectType: ThreadConfined { + self.storage = Storage(Results(RLMResults.emptyDetached()), subscription, keyPaths) + self.storage.configuration = configuration + self.filter = filter + self.sortDescriptor = sortDescriptor + } + + /** + Initialize a `ObservedQueryResults` struct for a given `Object` or `EmbeddedObject` type. + - parameter type: Observed type + - parameter subscription: The query used for the subscription. + - 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 configuration is set to the `defaultConfiguration` + - parameter filter: Observations will be made only for passing objects. + If no filter given - all objects will be observed + - parameter keyPaths: Only properties contained in the key paths array will be observed. + If `nil`, notifications will be delivered for any property change on the object. + String key paths which do not correspond to a valid a property will throw an exception. + - parameter sortDescriptor: A sequence of `SortDescriptor`s to sort by + */ + public init(_ type: ResultType.Type, + subscription: ((Query) -> Query)? = nil, + configuration: Realm.Configuration? = nil, + filter: NSPredicate? = nil, + keyPaths: [String]? = nil, + sortDescriptor: SortDescriptor? = nil) where ResultType: Object { + self.storage = Storage(Results(RLMResults.emptyDetached()), subscription, keyPaths) + self.storage.configuration = configuration + self.filter = filter + self.sortDescriptor = sortDescriptor + } +#if swift(>=5.5) + /** + Initialize a `ObservedQueryResults` struct for a given `Object` or `EmbeddedObject` type. + - parameter type: Observed type + - parameter subscription: The query used for the subscription. + - 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 configuration is set to the `defaultConfiguration` + - parameter where: Observations will be made only for passing objects. + If no type safe query is given - all objects will be observed + - parameter keyPaths: Only properties contained in the key paths array will be observed. + If `nil`, notifications will be delivered for any property change on the object. + String key paths which do not correspond to a valid a property will throw an exception. + - parameter sortDescriptor: A sequence of `SortDescriptor`s to sort by + */ + public init(_ type: ResultType.Type, + subscription: ((Query) -> Query)? = nil, + configuration: Realm.Configuration? = nil, + where: ((Query) -> Query)? = nil, + keyPaths: [String]? = nil, + sortDescriptor: SortDescriptor? = nil) where ResultType: Object { + self.storage = Storage(Results(RLMResults.emptyDetached()), subscription, keyPaths) + self.storage.configuration = configuration + self.where = `where` + self.sortDescriptor = sortDescriptor + } +#endif + /// :nodoc: + public init(_ type: ResultType.Type, + subscription: ((Query) -> Query)? = nil, + keyPaths: [String]? = nil, + configuration: Realm.Configuration? = nil, + sortDescriptor: SortDescriptor? = nil) where ResultType: Object { + self.storage = Storage(Results(RLMResults.emptyDetached()), subscription, keyPaths) + self.storage.configuration = configuration + self.sortDescriptor = sortDescriptor + } + + public mutating func update() { + // When the view updates, it will inject the @Environment + // into the propertyWrapper + if storage.configuration == nil { + storage.configuration = configuration + } + } +} +#endif // canImport(_Concurrency) + // MARK: ObservedRealmObject /// A property wrapper type that subscribes to an observable Realm `Object` or `List` and @@ -707,6 +1003,10 @@ extension Projection: _ObservedResultsValue { } _storage = ObservedObject(wrappedValue: ObservableStorage(wrappedValue)) defaultValue = ObjectType(projecting: ObjectType.Root()) } + +// public init(wrappedValue: ObjectType) where ObjectType: QueryResults { +// _storage = ObservedObject(wrappedValue: ObservableStorage(wrappedValue)) +// } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) @@ -1166,33 +1466,33 @@ private class ObservableAsyncOpenStorage: ObservableObject { } }.store(in: &appCancellable) } +} - // MARK: - AutoOpen & AsyncOpen Helper +// MARK: - AutoOpen & AsyncOpen Helper - class func configureApp(appId: String? = nil, withTimeout timeout: UInt? = nil) -> App { - var app: App - if let appId = appId { - app = App(id: appId) - } else { - // Check if there is a singular cached app - let cachedApps = RLMApp.allApps() - if cachedApps.count > 1 { - throwRealmException("Cannot AsyncOpen the Realm because more than one appId was found. When using multiple Apps you must explicitly pass an appId to indicate which to use.") - } - guard let cachedApp = cachedApps.first else { - throwRealmException("Cannot AsyncOpen the Realm because no appId was found. You must either explicitly pass an appId or initialize an App before displaying your View.") - } - app = cachedApp +private func configureApp(appId: String? = nil, withTimeout timeout: UInt? = nil) -> App { + var app: App + if let appId = appId { + app = App(id: appId) + } else { + // Check if there is a singular cached app + let cachedApps = RLMApp.allApps() + if cachedApps.count > 1 { + throwRealmException("Cannot AsyncOpen the Realm because more than one appId was found. When using multiple Apps you must explicitly pass an appId to indicate which to use.") } - - // Setup timeout if needed - if let timeout = timeout { - let syncTimeoutOptions = SyncTimeoutOptions() - syncTimeoutOptions.connectTimeout = timeout - app.syncManager.timeoutOptions = syncTimeoutOptions + guard let cachedApp = cachedApps.first else { + throwRealmException("Cannot AsyncOpen the Realm because no appId was found. You must either explicitly pass an appId or initialize an App before displaying your View.") } - return app + app = cachedApp + } + + // Setup timeout if needed + if let timeout = timeout { + let syncTimeoutOptions = SyncTimeoutOptions() + syncTimeoutOptions.connectTimeout = timeout + app.syncManager.timeoutOptions = syncTimeoutOptions } + return app } // MARK: - AsyncOpen @@ -1274,7 +1574,7 @@ private class ObservableAsyncOpenStorage: ObservableObject { partitionValue: Partition, configuration: Realm.Configuration? = nil, timeout: UInt? = nil) { - let app = ObservableAsyncOpenStorage.configureApp(appId: appId, withTimeout: timeout) + let app = configureApp(appId: appId, withTimeout: timeout) // Store property wrapper values on the storage storage = ObservableAsyncOpenStorage(asyncOpenKind: .asyncOpen, app: app, configuration: configuration, partitionValue: AnyBSON(partitionValue)) } @@ -1382,7 +1682,7 @@ private class ObservableAsyncOpenStorage: ObservableObject { partitionValue: Partition, configuration: Realm.Configuration? = nil, timeout: UInt? = nil) { - let app = ObservableAsyncOpenStorage.configureApp(appId: appId, withTimeout: timeout) + let app = configureApp(appId: appId, withTimeout: timeout) // Store property wrapper values on the storage storage = ObservableAsyncOpenStorage(asyncOpenKind: .autoOpen, app: app, configuration: configuration, partitionValue: AnyBSON(partitionValue)) } diff --git a/RealmSwift/Sync.swift b/RealmSwift/Sync.swift index b20205ab974..ec9006913a6 100644 --- a/RealmSwift/Sync.swift +++ b/RealmSwift/Sync.swift @@ -929,8 +929,9 @@ extension User { @return A `Realm.Configuration` instance with a flexible sync configuration. */ - public func flexibleSyncConfiguration() -> Realm.Configuration { - let config = self.__flexibleSyncConfiguration() + public func flexibleSyncConfiguration(_ configuration: Realm.Configuration = Realm.Configuration()) -> Realm.Configuration { + let config = configuration.rlmConfiguration + config.syncConfiguration = self.__flexibleSyncConfiguration().syncConfiguration return ObjectiveCSupport.convert(object: config) } } diff --git a/RealmSwift/SyncSubscription.swift b/RealmSwift/SyncSubscription.swift index 80da15f68b4..ebf269d306a 100644 --- a/RealmSwift/SyncSubscription.swift +++ b/RealmSwift/SyncSubscription.swift @@ -50,111 +50,192 @@ import Realm.Private } } -/** - `SyncSubscription` is used to define a Flexible Sync subscription obtained from querying a - subscription set, which can be used to read or remove/update a committed subscription. - */ -@frozen public struct SyncSubscription { +public protocol _SyncSubscription { + associatedtype Element: RealmCollectionValue - // MARK: Initializers - fileprivate let _rlmSyncSubscription: RLMSyncSubscription + /// Query string of the subscription. + var query: String { get } - fileprivate init(_ rlmSyncSubscription: RLMSyncSubscription) { - self._rlmSyncSubscription = rlmSyncSubscription - } + /// When the subscription was created. Recorded automatically. + var createdAt: Date { get } + + /// When the subscription was last updated. Recorded automatically. + var updatedAt: Date { get} + + #if swift(>=5.6) && canImport(_Concurrency) + @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) + /** + Removes the current subscription from the subscription set, associated to this `QueryResults`. + + - throws: An `NSError` if t + */ + func unsubscribe() async throws + #endif // canImport(_Concurrency) +} + +// `SyncSubscription` includes all the common implementation for a subscription. +internal protocol SyncSubscription: _SyncSubscription { + var _rlmSyncSubscription: RLMSyncSubscription? { get } + init(_ rlmSyncSubscription: RLMSyncSubscription, _ results: Results) +} - /// Name of the subscription, if not specified it will return the value in Query as a String. - public var name: String? { - _rlmSyncSubscription.name +extension SyncSubscription { + /// Query string of the subscription. + public var query: String { + _rlmSyncSubscription!.queryString } /// When the subscription was created. Recorded automatically. public var createdAt: Date { - _rlmSyncSubscription.createdAt + _rlmSyncSubscription!.createdAt } /// When the subscription was last updated. Recorded automatically. public var updatedAt: Date { - _rlmSyncSubscription.updatedAt + _rlmSyncSubscription!.updatedAt } +} +#if swift(>=5.6) && canImport(_Concurrency) +@available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) +extension SyncSubscription { /** - Updates a Flexible Sync's subscription with an allowed query which will be used to bootstrap data - from the server when committed. - - - warning: This method may only be called during a write subscription block. - - - parameter type: The type of the object to be queried. - - parameter query: A query which will be used to modify the query. + Removes the current subscription from the subscription set, associated to this `QueryResults`. */ - public func update(toType type: T.Type, where query: @escaping (Query) -> Query) { - guard _rlmSyncSubscription.objectClassName == "\(T.self)" else { - throwRealmException("Updating a subscription query of a different Object Type is not allowed.") + public func unsubscribe() async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let completion: (Error?) -> Void = { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + _rlmSyncSubscription?.unsubscribe(onComplete: completion) } - _rlmSyncSubscription.update(with: query(Query()).predicate) } +} +#endif // canImport(_Concurrency) - /** - Updates a Flexible Sync's subscription with an allowed query which will be used to bootstrap data - from the server when committed. +/** + A type-erased `QueryResults`. +*/ +public struct AnyQueryResults: SyncSubscription, Sequence { + + public typealias Element = DynamicObject + + internal var results: Results + internal var _rlmSyncSubscription: RLMSyncSubscription? + + init(_ rlmSyncSubscription: RLMSyncSubscription, _ results: Results) { + self._rlmSyncSubscription = rlmSyncSubscription + self.results = results + } + + /// A human-readable description of the objects represented by the results.. + public var description: String { + return RLMDescriptionWithMaxDepth("AnyQueryResults", results.collection, RLMDescriptionMaxDepth) + } + + // MARK: Sequence - - warning: This method may only be called during a write subscription block. + /// Returns an iterator over the elements of this sequence. + public func makeIterator() -> Results.Iterator { + results.makeIterator() + } + + /** + Returns a `QueryResults` for the given type, returns nil if the results doesn't correspond to the given type. - - parameter predicateFormat: A predicate format string, optionally followed by a variable number of arguments, - which will be used to modify the query. + - parameter type: The type of the results to return. */ - public func update(to predicateFormat: String, _ args: Any...) { - _rlmSyncSubscription.update(with: NSPredicate(format: predicateFormat, argumentArray: unwrapOptionals(in: args))) + public func `as`(type: T.Type) -> QueryResults? { + guard _rlmSyncSubscription!.objectClassName == "\(T.self)" else { + return nil + } + return QueryResults(_rlmSyncSubscription!, results.realm!.objects(T.self).filter(_rlmSyncSubscription!.queryString)) } +} - /** - Updates a Flexible Sync's subscription with an allowed query which will be used to bootstrap data - from the server when committed. +/** + `QueryResults` wraps a sync subscription and contains the data for the subscription's query, + + `QueryResults` works exactly like `Results`and lazily evaluates only the first time it is accessed, + and allows all the operations and subqueries over the results. + */ +@frozen public struct QueryResults: SyncSubscription, RealmCollectionImpl, Equatable where ElementType: RealmCollectionValue { + public typealias Element = ElementType + + internal var _rlmSyncSubscription: RLMSyncSubscription? + internal var results: Results? + internal let collection: RLMCollection + + /// A human-readable description of the objects represented by the results. + public var description: String { + return RLMDescriptionWithMaxDepth("QueryResults", collection, RLMDescriptionMaxDepth) + } + + // MARK: Initializers + + init(collection: RLMCollection) { + self.collection = collection + } - - warning: This method may only be called during a write subscription block. + init(_ rlmSyncSubscription: RLMSyncSubscription, _ results: Results) { + self._rlmSyncSubscription = rlmSyncSubscription + self.results = results + self.collection = results.collection + } - - parameter predicate: The predicate with which to filter the objects on the server, which - will be used to modify the query. + // MARK: Object Retrieval + /** + Returns the object at the given `index`. + - parameter index: The index. */ - public func update(to predicate: NSPredicate) { - _rlmSyncSubscription.update(with: predicate) + public subscript(position: Int) -> Element { + throwForNegativeIndex(position) + return staticBridgeCast(fromObjectiveC: collection.object(at: UInt(position))) + } + + // MARK: Equatable + + public static func == (lhs: QueryResults, rhs: QueryResults) -> Bool { + lhs.collection.isEqual(rhs.collection) } } +extension QueryResults: Encodable where Element: Encodable {} + +protocol _QuerySubscription { + var className: String { get } + var predicate: NSPredicate { get } +} + /** - `SubscriptionQuery` is used to define an named/unnamed query subscription query, which - can be added/remove or updated within a write subscription transaction. + `QuerySubscription` is used to define a subscription query, used to be able to add a query to a subscription set. */ -@frozen public struct QuerySubscription { +@frozen public struct QuerySubscription: _QuerySubscription { // MARK: Internal - fileprivate let name: String? - fileprivate var className: String - fileprivate var predicate: NSPredicate - - /// :nodoc: - public typealias QueryFunction = (Query) -> Query + internal var className: String + internal var predicate: NSPredicate /** Creates a `QuerySubscription` for the given type. - - parameter name: Name of the subscription. - - parameter query: The query for the subscription. + - parameter query: The query for the subscription. if nil it will set the query to all documents for the collection. */ - public init(name: String? = nil, query: @escaping QueryFunction) { - self.name = name + public init(_ query: ((Query) -> Query)? = nil) { self.className = "\(T.self)" - self.predicate = query(Query()).predicate + self.predicate = query?(Query()).predicate ?? NSPredicate(format: "TRUEPREDICATE") } /** Creates a `QuerySubscription` for the given type. - - parameter name: Name of the subscription. - parameter predicateFormat: A predicate format string, optionally followed by a variable number of arguments, which will be used to create the subscription. */ - public init(name: String? = nil, where predicateFormat: String, _ args: Any...) { - self.name = name + public init(_ predicateFormat: String, _ args: Any...) { self.className = "\(T.self)" self.predicate = NSPredicate(format: predicateFormat, argumentArray: unwrapOptionals(in: args)) } @@ -162,11 +243,9 @@ import Realm.Private /** Creates a `QuerySubscription` for the given type. - - parameter name: Name of the subscription. - parameter predicate: The predicate defining the query used to filter the objects on the server.. */ - public init(name: String? = nil, where predicate: NSPredicate) { - self.name = name + public init(_ predicate: NSPredicate) { self.className = "\(T.self)" self.predicate = predicate } @@ -174,32 +253,24 @@ import Realm.Private /** `SyncSubscriptionSet` is a collection of `SyncSubscription`s. This is the entry point - for adding and removing `SyncSubscription`s. + for adding `SyncSubscription`s. */ @frozen public struct SyncSubscriptionSet { // MARK: Internal - internal let rlmSyncSubscriptionSet: RLMSyncSubscriptionSet + private let rlmSyncSubscriptionSet: RLMSyncSubscriptionSet + private let realm: Realm // MARK: Initializers - internal init(_ rlmSyncSubscriptionSet: RLMSyncSubscriptionSet) { + internal init(_ rlmSyncSubscriptionSet: RLMSyncSubscriptionSet, realm: Realm) { self.rlmSyncSubscriptionSet = rlmSyncSubscriptionSet + self.realm = realm } - /// The number of subscriptions in the subscription set. - public var count: Int { return Int(rlmSyncSubscriptionSet.count) } - - /** - Synchronously performs any transactions (add/remove/update) to the subscription set within the block. - This will not wait for the server to acknowledge and see all the data associated with this collection of subscriptions, - and will return after committing the subscription transactions. + // MARK: Private - - parameter block: The block containing the subscriptions transactions to perform. - - parameter onComplete: The block called upon synchronization of subscriptions to the server. Otherwise - an `Error`describing what went wrong will be returned by the block - */ - public func write(_ block: (() -> Void), onComplete: ((Error?) -> Void)? = nil) { + private func write(_ block: (() -> Void), onComplete: ((Error?) -> Void)? = nil) { rlmSyncSubscriptionSet.write(block, onComplete: onComplete ?? { _ in }) } @@ -220,235 +291,557 @@ import Realm.Private } /** - Returns a subscription by the specified name. - - - parameter named: The name of the subscription searching for. - - returns: A subscription for the given name. - */ - public func first(named: String) -> SyncSubscription? { - return rlmSyncSubscriptionSet.subscription(withName: named).map(SyncSubscription.init) - } - - /** - Returns a subscription by the specified query. + Returns a `QueryResults` for the specified query. - parameter type: The type of the object to be queried. - parameter where: A query builder that produces a subscription which can be used to search the subscription by query and/or name. - - returns: A query builder that produces a subscription which can used to search for the subscription. + - returns: `QueryResults` for the given query containing the data for the query. */ - public func first(ofType type: T.Type, `where` query: @escaping (Query) -> Query) -> SyncSubscription? { - return rlmSyncSubscriptionSet.subscription(withClassName: "\(T.self)", predicate: query(Query()).predicate).map(SyncSubscription.init) + public func first(ofType type: T.Type, `where` query: @escaping (Query) -> Query) -> QueryResults? { + let predicate = query(Query()).predicate + return rlmSyncSubscriptionSet.subscription(withClassName: "\(T.self)", predicate: predicate).map { + QueryResults($0, realm.objects(T.self).filter(predicate)) + } } + /// The number of subscriptions in the subscription set. + public var count: Int { return Int(rlmSyncSubscriptionSet.count) } + + // MARK: Subscription Retrieval + /** - Returns a subscription by the specified query. + Returns a `AnyQueryResults`representing the query results at the given `position`. - - parameter type: The type of the object to be queried. - - parameter where: A query builder that produces a subscription which can be used to search - the subscription by query and/or name. - - returns: A query builder that produces a subscription which can used to search for the subscription. + - parameter position: The index for the resulting subscription. */ - public func first(ofType type: T.Type, `where` predicateFormat: String, _ args: Any...) -> SyncSubscription? { - return rlmSyncSubscriptionSet.subscription(withClassName: "\(T.self)", predicate: NSPredicate(format: predicateFormat, argumentArray: unwrapOptionals(in: args))).map(SyncSubscription.init) + public subscript(position: Int) -> AnyQueryResults? { + throwForNegativeIndex(position) + return rlmSyncSubscriptionSet.object(at: UInt(position)).map { + AnyQueryResults($0, realm.dynamicObjects($0.objectClassName)) + } } - /** - Returns a subscription by the specified query. + /// Returns a `AnyQueryResults` representing the first object in the subscription set list, or `nil` if there is no subscriptions. + public var first: AnyQueryResults? { + return rlmSyncSubscriptionSet.firstObject().map { + AnyQueryResults($0, realm.dynamicObjects($0.objectClassName)) + } + } - - parameter type: The type of the object to be queried. - - parameter where: A query builder that produces a subscription which can be used to search - the subscription by query and/or name. - - returns: A query builder that produces a subscription which can used to search for the subscription. - */ - public func first(ofType type: T.Type, `where` predicate: NSPredicate) -> SyncSubscription? { - return rlmSyncSubscriptionSet.subscription(withClassName: "\(T.self)", predicate: predicate).map(SyncSubscription.init) + /// Returns a `AnyQueryResults` representing the last object in the subscription set list, or `nil` if there is no subscriptions. + public var last: AnyQueryResults? { + return rlmSyncSubscriptionSet.lastObject().map { + AnyQueryResults($0, realm.dynamicObjects($0.objectClassName)) + } } +} - /** - Appends one or several subscriptions to the subscription set. +extension SyncSubscriptionSet: Sequence { + // MARK: Sequence Support - - warning: This method may only be called during a write subscription block. + /// Returns a `SyncSubscriptionSetIterator` that yields successive elements in the subscription collection. + public func makeIterator() -> SyncSubscriptionSetIterator { + return SyncSubscriptionSetIterator(rlmSyncSubscriptionSet, realm) + } +} - - parameter subscriptions: The subscriptions to be added to the subscription set. - */ - public func `append`(_ subscriptions: QuerySubscription...) { - subscriptions.forEach { subscription in - rlmSyncSubscriptionSet.addSubscription(withClassName: subscription.className, - subscriptionName: subscription.name, - predicate: subscription.predicate) +/** + This struct enables sequence-style enumeration for `SyncSubscriptionSet`. + */ +@frozen public struct SyncSubscriptionSetIterator: IteratorProtocol { + private let rlmSubscriptionSet: RLMSyncSubscriptionSet + private let realm: Realm + private var index: Int = -1 + + init(_ rlmSubscriptionSet: RLMSyncSubscriptionSet, _ realm: Realm) { + self.rlmSubscriptionSet = rlmSubscriptionSet + self.realm = realm + } + + private func nextIndex(for index: Int?) -> Int? { + if let index = index, index < self.rlmSubscriptionSet.count - 1 { + return index + 1 + } + return nil + } + + mutating public func next() -> AnyQueryResults? { + if let index = self.nextIndex(for: self.index) { + self.index = index + return rlmSubscriptionSet.object(at: UInt(index)).map { + AnyQueryResults($0, realm.dynamicObjects($0.objectClassName)) + } } + return nil } +} +#if swift(>=5.6) && canImport(_Concurrency) +@available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) +extension SyncSubscriptionSet { /** - Removes a subscription with the specified query. + Asynchronously creates and commit a write transaction and updates the subscription set, + this will not wait for the server to acknowledge and see all the data associated with this + collection of subscription. - - warning: This method may only be called during a write subscription block. + - parameter block: The block containing the subscriptions transactions to perform. - - parameter type: The type of the object to be removed. - - parameter to: A query for the subscription to be removed from the subscription set. + - throws: An `NSError` if the transaction could not be completed successfully. + If `block` throws, the function throws the propagated `ErrorType` instead. */ - public func remove(ofType type: T.Type, _ query: @escaping (Query) -> Query) { - rlmSyncSubscriptionSet.removeSubscription(withClassName: "\(T.self)", - predicate: query(Query()).predicate) + @MainActor + private func write(_ block: (() -> Void)) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + write(block) { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + } + + @MainActor + private func subscribe(_ subscriptions: _QuerySubscription...) async throws { + try await write { + subscriptions.forEach { subscription in + rlmSyncSubscriptionSet.addSubscription(withClassName: subscription.className, + predicate: subscription.predicate) + } + } } /** - Removes a subscription with the specified query. + Appends the query to the current subscription set and wait for the server to acknowledge the subscription, + returns a `QueryResults` containing all the data associated to this query. - - warning: This method may only be called during a write subscription block. + - parameter query: The query which will be used for the subscription. + - returns: `QueryResults` for the given subscription containing the data for the query. - - parameter type: The type of the object to be removed. - - parameter predicateFormat: A predicate format string, optionally followed by a variable number of arguments, - which will be used to identify the subscription to be removed. + - throws: An `NSError` if the subscription couldn't be completed by the client or server. */ - public func remove(ofType type: T.Type, where predicateFormat: String, _ args: Any...) { - rlmSyncSubscriptionSet.removeSubscription(withClassName: "\(T.self)", - predicate: NSPredicate(format: predicateFormat, argumentArray: unwrapOptionals(in: args))) + public func subscribe(to query: @escaping ((Query) -> Query)) async throws -> QueryResults { + let query = QuerySubscription(query) + try await subscribe(query) + return QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query.className, predicate: query.predicate)!, + realm.objects(T.self).filter(query.predicate)) } /** - Removes a subscription with the specified query. + Appends the query to the current subscription set and wait for the server to acknowledge the subscription, + returns a `QueryResults` containing all the data associated to this object type. - - warning: This method may only be called during a write subscription block. + - parameter type: The type of the object to be queried,. + - returns: `QueryResults` for the given subscription containing the data for the query. - - parameter type: The type of the object to be removed. - - parameter predicate: The predicate which will be used to identify the subscription to be removed. + - throws: An `NSError` if the subscription couldn't be completed by the client or server. */ - public func remove(ofType type: T.Type, where predicate: NSPredicate) { - rlmSyncSubscriptionSet.removeSubscription(withClassName: "\(T.self)", - predicate: predicate) + public func subscribe(to type: T.Type) async throws -> QueryResults { + let query = QuerySubscription() + try await subscribe(query) + return QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query.className, predicate: query.predicate)!, + realm.objects(T.self)) } /** - Removes one or several subscriptions from the subscription set. + Appends the query to the current subscription set and wait for the server to acknowledge the subscription, + returns a tuple of `QueryResults`s containing all the data associated to this queries. - - warning: This method may only be called during a write subscription block. + - parameter query: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query2: A `QuerySubscription` representing the query which will be used for the subscription. + - returns: A tuple of `QueryResults`s for the given subscriptions containing the data for each query. - - parameter subscription: The subscription to be removed from the subscription set. + - throws: An `NSError` if the subscription couldn't be completed by the client or server. */ - public func remove(_ subscriptions: SyncSubscription...) { - subscriptions.forEach { subscription in - rlmSyncSubscriptionSet.remove(subscription._rlmSyncSubscription) - } + public func subscribe(to query: QuerySubscription, _ query2: QuerySubscription) async throws -> (QueryResults, QueryResults) { + try await subscribe(query, query2) + return (QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query.className, predicate: query.predicate)!, + realm.objects(T1.self).filter(query.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query2.className, predicate: query2.predicate)!, + realm.objects(T2.self).filter(query2.predicate))) } /** - Removes a subscription with the specified name from the subscription set. + Appends the query to the current subscription set and wait for the server to acknowledge the subscription, + returns a tuple of `QueryResults`s containing all the data associated to this queries. - - warning: This method may only be called during a write subscription block. + - parameter query: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query2: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query3: A `QuerySubscription` representing the query which will be used for the subscription. + - returns: A tuple of `QueryResults`s for the given subscriptions containing the data for each query. - - parameter named: The name of the subscription to be removed from the subscription set. + - throws: An `NSError` if the subscription couldn't be completed by the client or server. */ - public func remove(named: String) { - rlmSyncSubscriptionSet.removeSubscription(withName: named) + // swiftlint:disable large_tuple + public func subscribe(to query: QuerySubscription, _ query2: QuerySubscription, _ query3: QuerySubscription) async throws -> (QueryResults, QueryResults, QueryResults) { + try await subscribe(query, query2, query3) + return (QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query.className, predicate: query.predicate)!, + realm.objects(T1.self).filter(query.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query2.className, predicate: query2.predicate)!, + realm.objects(T2.self).filter(query2.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query3.className, predicate: query3.predicate)!, + realm.objects(T3.self).filter(query3.predicate))) } /** - Removes all subscriptions from the subscription set. + Appends the query to the current subscription set and wait for the server to acknowledge the subscription, + returns a tuple of `QueryResults`s containing all the data associated to this queries. + + - parameter query: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query2: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query3: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query4: A `QuerySubscription` representing the query which will be used for the subscription. + - returns: A tuple of `QueryResults`s for the given subscriptions containing the data for each query. - - warning: This method may only be called during a write subscription block. - - warning: Removing all subscriptions will result in an error if no new subscription is added. Server should - acknowledge at least one subscription. + - throws: An `NSError` if the subscription couldn't be completed by the client or server. */ - public func removeAll() { - rlmSyncSubscriptionSet.removeAllSubscriptions() + // swiftlint:disable large_tuple + public func subscribe(to query: QuerySubscription, _ query2: QuerySubscription, _ query3: QuerySubscription, _ query4: QuerySubscription) async throws -> (QueryResults, QueryResults, QueryResults, QueryResults) { + try await subscribe(query, query2, query3, query4) + return (QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query.className, predicate: query.predicate)!, + realm.objects(T1.self).filter(query.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query2.className, predicate: query2.predicate)!, + realm.objects(T2.self).filter(query2.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query3.className, predicate: query3.predicate)!, + realm.objects(T3.self).filter(query3.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query4.className, predicate: query4.predicate)!, + realm.objects(T4.self).filter(query4.predicate))) } /** - Removes zero or none subscriptions of the given type from the subscription set. + Appends the query to the current subscription set and wait for the server to acknowledge the subscription, + returns a tuple of `QueryResults`s containing all the data associated to this queries. - - warning: This method may only be called during a write subscription block. + - parameter query: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query2: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query3: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query4: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query5: A `QuerySubscription` representing the query which will be used for the subscription. + - returns: A tuple of `QueryResults`s for the given subscriptions containing the data for each query. - - parameter type: The type of the objects to be removed. + - throws: An `NSError` if the subscription couldn't be completed by the client or server. */ - public func removeAll(ofType type: T.Type) { - rlmSyncSubscriptionSet.removeAllSubscriptions(withClassName: type.className()) + // swiftlint:disable large_tuple + public func subscribe(to query: QuerySubscription, _ query2: QuerySubscription, _ query3: QuerySubscription, _ query4: QuerySubscription, _ query5: QuerySubscription) async throws -> (QueryResults, QueryResults, QueryResults, QueryResults, QueryResults) { + try await subscribe(query, query2, query3, query4, query5) + return (QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query.className, predicate: query.predicate)!, + realm.objects(T1.self).filter(query.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query2.className, predicate: query2.predicate)!, + realm.objects(T2.self).filter(query2.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query3.className, predicate: query3.predicate)!, + realm.objects(T3.self).filter(query3.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query4.className, predicate: query4.predicate)!, + realm.objects(T4.self).filter(query4.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query5.className, predicate: query5.predicate)!, + realm.objects(T5.self).filter(query5.predicate))) } - // MARK: Subscription Retrieval - /** - Returns the subscription at the given `position`. - - - parameter position: The index for the resulting subscription. + Appends the query to the current subscription set and wait for the server to acknowledge the subscription, + returns a tuple of `QueryResults`s containing all the data associated to this queries. + + - parameter query: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query2: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query3: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query4: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query5: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query6: A `QuerySubscription` representing the query which will be used for the subscription. + - returns: A tuple of `QueryResults`s for the given subscriptions containing the data for each query. + + - throws: An `NSError` if the subscription couldn't be completed by the client or server. */ - public subscript(position: Int) -> SyncSubscription? { - throwForNegativeIndex(position) - return rlmSyncSubscriptionSet.object(at: UInt(position)).map { SyncSubscription($0) } + // swiftlint:disable large_tuple + public func subscribe(to query: QuerySubscription, _ query2: QuerySubscription, _ query3: QuerySubscription, _ query4: QuerySubscription, _ query5: QuerySubscription, _ query6: QuerySubscription) async throws -> (QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults) { + try await subscribe(query, query2, query3, query4, query5, query6) + return (QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query.className, predicate: query.predicate)!, + realm.objects(T1.self).filter(query.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query2.className, predicate: query2.predicate)!, + realm.objects(T2.self).filter(query2.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query3.className, predicate: query3.predicate)!, + realm.objects(T3.self).filter(query3.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query4.className, predicate: query4.predicate)!, + realm.objects(T4.self).filter(query4.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query5.className, predicate: query5.predicate)!, + realm.objects(T5.self).filter(query5.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query6.className, predicate: query6.predicate)!, + realm.objects(T6.self).filter(query6.predicate))) } - /// Returns the first object in the SyncSubscription list, or `nil` if the subscriptions are empty. - public var first: SyncSubscription? { - return rlmSyncSubscriptionSet.firstObject().map { SyncSubscription($0) } + /** + Appends the query to the current subscription set and wait for the server to acknowledge the subscription, + returns a tuple of `QueryResults`s containing all the data associated to this queries. + + - parameter query: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query2: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query3: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query4: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query5: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query6: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query7: A `QuerySubscription` representing the query which will be used for the subscription. + - returns: A tuple of `QueryResults`s for the given subscriptions containing the data for each query. + + - throws: An `NSError` if the subscription couldn't be completed by the client or server. + */ + // swiftlint:disable large_tuple + public func subscribe(to query: QuerySubscription, _ query2: QuerySubscription, _ query3: QuerySubscription, _ query4: QuerySubscription, _ query5: QuerySubscription, _ query6: QuerySubscription, _ query7: QuerySubscription) async throws -> (QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults) { + try await subscribe(query, query2, query3, query4, query5, query6, query7) + return (QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query.className, predicate: query.predicate)!, + realm.objects(T1.self).filter(query.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query2.className, predicate: query2.predicate)!, + realm.objects(T2.self).filter(query2.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query3.className, predicate: query3.predicate)!, + realm.objects(T3.self).filter(query3.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query4.className, predicate: query4.predicate)!, + realm.objects(T4.self).filter(query4.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query5.className, predicate: query5.predicate)!, + realm.objects(T5.self).filter(query5.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query6.className, predicate: query6.predicate)!, + realm.objects(T6.self).filter(query6.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query7.className, predicate: query7.predicate)!, + realm.objects(T7.self).filter(query7.predicate))) } - /// Returns the last object in the SyncSubscription list, or `nil` if the subscriptions are empty. - public var last: SyncSubscription? { - return rlmSyncSubscriptionSet.lastObject().map { SyncSubscription($0) } + /** + Appends the query to the current subscription set and wait for the server to acknowledge the subscription, + returns a tuple of `QueryResults`s containing all the data associated to this queries. + + - parameter query: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query2: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query3: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query4: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query5: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query6: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query7: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query8: A `QuerySubscription` representing the query which will be used for the subscription. + - returns: A tuple of `QueryResults`s for the given subscriptions containing the data for each query. + + - throws: An `NSError` if the subscription couldn't be completed by the client or server. + */ + // swiftlint:disable large_tuple + public func subscribe(to query: QuerySubscription, _ query2: QuerySubscription, _ query3: QuerySubscription, _ query4: QuerySubscription, _ query5: QuerySubscription, _ query6: QuerySubscription, _ query7: QuerySubscription, _ query8: QuerySubscription) async throws -> (QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults) { + try await subscribe(query, query2, query3, query4, query5, query6, query7, query8) + return (QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query.className, predicate: query.predicate)!, + realm.objects(T1.self).filter(query.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query2.className, predicate: query2.predicate)!, + realm.objects(T2.self).filter(query2.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query3.className, predicate: query3.predicate)!, + realm.objects(T3.self).filter(query3.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query4.className, predicate: query4.predicate)!, + realm.objects(T4.self).filter(query4.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query5.className, predicate: query5.predicate)!, + realm.objects(T5.self).filter(query5.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query6.className, predicate: query6.predicate)!, + realm.objects(T6.self).filter(query6.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query7.className, predicate: query7.predicate)!, + realm.objects(T7.self).filter(query7.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query8.className, predicate: query8.predicate)!, + realm.objects(T8.self).filter(query8.predicate))) } -} - -extension SyncSubscriptionSet: Sequence { - // MARK: Sequence Support - /// Returns a `SyncSubscriptionSetIterator` that yields successive elements in the subscription collection. - public func makeIterator() -> SyncSubscriptionSetIterator { - return SyncSubscriptionSetIterator(rlmSyncSubscriptionSet) + /** + Appends the query to the current subscription set and wait for the server to acknowledge the subscription, + returns a tuple of `QueryResults`s containing all the data associated to this queries. + + - parameter query: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query2: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query3: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query4: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query5: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query6: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query7: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query8: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query9: A `QuerySubscription` representing the query which will be used for the subscription. + - returns: A tuple of `QueryResults`s for the given subscriptions containing the data for each query. + + - throws: An `NSError` if the subscription couldn't be completed by the client or server. + */ + // swiftlint:disable large_tuple + public func subscribe(to query: QuerySubscription, _ query2: QuerySubscription, _ query3: QuerySubscription, _ query4: QuerySubscription, _ query5: QuerySubscription, _ query6: QuerySubscription, _ query7: QuerySubscription, _ query8: QuerySubscription, _ query9: QuerySubscription) async throws -> (QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults) { + try await subscribe(query, query2, query3, query4, query5, query6, query7, query8, query9) + return (QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query.className, predicate: query.predicate)!, + realm.objects(T1.self).filter(query.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query2.className, predicate: query2.predicate)!, + realm.objects(T2.self).filter(query2.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query3.className, predicate: query3.predicate)!, + realm.objects(T3.self).filter(query3.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query4.className, predicate: query4.predicate)!, + realm.objects(T4.self).filter(query4.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query5.className, predicate: query5.predicate)!, + realm.objects(T5.self).filter(query5.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query6.className, predicate: query6.predicate)!, + realm.objects(T6.self).filter(query6.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query7.className, predicate: query7.predicate)!, + realm.objects(T7.self).filter(query7.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query8.className, predicate: query8.predicate)!, + realm.objects(T8.self).filter(query8.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query9.className, predicate: query9.predicate)!, + realm.objects(T9.self).filter(query9.predicate))) } -} -/** - This struct enables sequence-style enumeration for `SyncSubscriptionSet`. - */ -@frozen public struct SyncSubscriptionSetIterator: IteratorProtocol { - private let rlmSubscriptionSet: RLMSyncSubscriptionSet - private var index: Int = -1 + /** + Appends the query to the current subscription set and wait for the server to acknowledge the subscription, + returns a tuple of `QueryResults`s containing all the data associated to this queries. + + - parameter query: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query2: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query3: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query4: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query5: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query6: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query7: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query8: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query9: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query10: A `QuerySubscription` representing the query which will be used for the subscription. + - returns: A tuple of `QueryResults`s for the given subscriptions containing the data for each query. + + - throws: An `NSError` if the subscription couldn't be completed by the client or server. + */ + // swiftlint:disable large_tuple + public func subscribe(to query: QuerySubscription, _ query2: QuerySubscription, _ query3: QuerySubscription, _ query4: QuerySubscription, _ query5: QuerySubscription, _ query6: QuerySubscription, _ query7: QuerySubscription, _ query8: QuerySubscription, _ query9: QuerySubscription, _ query10: QuerySubscription) async throws -> (QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults) { + try await subscribe(query, query2, query3, query4, query5, query6, query7, query8, query9, query10) + return (QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query.className, predicate: query.predicate)!, + realm.objects(T1.self).filter(query.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query2.className, predicate: query2.predicate)!, + realm.objects(T2.self).filter(query2.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query3.className, predicate: query3.predicate)!, + realm.objects(T3.self).filter(query3.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query4.className, predicate: query4.predicate)!, + realm.objects(T4.self).filter(query4.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query5.className, predicate: query5.predicate)!, + realm.objects(T5.self).filter(query5.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query6.className, predicate: query6.predicate)!, + realm.objects(T6.self).filter(query6.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query7.className, predicate: query7.predicate)!, + realm.objects(T7.self).filter(query7.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query8.className, predicate: query8.predicate)!, + realm.objects(T8.self).filter(query8.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query9.className, predicate: query9.predicate)!, + realm.objects(T9.self).filter(query9.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query10.className, predicate: query10.predicate)!, + realm.objects(T10.self).filter(query10.predicate))) + } - init(_ rlmSubscriptionSet: RLMSyncSubscriptionSet) { - self.rlmSubscriptionSet = rlmSubscriptionSet + /** + Appends the query to the current subscription set and wait for the server to acknowledge the subscription, + returns a tuple of `QueryResults`s containing all the data associated to this queries. + + - parameter query: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query2: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query3: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query4: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query5: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query6: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query7: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query8: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query9: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query10: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query11: A `QuerySubscription` representing the query which will be used for the subscription. + - returns: A tuple of `QueryResults`s for the given subscriptions containing the data for each query. + + - throws: An `NSError` if the subscription couldn't be completed by the client or server. + */ + // swiftlint:disable large_tuple + public func subscribe(to query: QuerySubscription, _ query2: QuerySubscription, _ query3: QuerySubscription, _ query4: QuerySubscription, _ query5: QuerySubscription, _ query6: QuerySubscription, _ query7: QuerySubscription, _ query8: QuerySubscription, _ query9: QuerySubscription, _ query10: QuerySubscription, _ query11: QuerySubscription) async throws -> (QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults) { + try await subscribe(query, query2, query3, query4, query5, query6, query7, query8, query9, query10, query11) + return (QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query.className, predicate: query.predicate)!, + realm.objects(T1.self).filter(query.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query2.className, predicate: query2.predicate)!, + realm.objects(T2.self).filter(query2.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query3.className, predicate: query3.predicate)!, + realm.objects(T3.self).filter(query3.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query4.className, predicate: query4.predicate)!, + realm.objects(T4.self).filter(query4.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query5.className, predicate: query5.predicate)!, + realm.objects(T5.self).filter(query5.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query6.className, predicate: query6.predicate)!, + realm.objects(T6.self).filter(query6.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query7.className, predicate: query7.predicate)!, + realm.objects(T7.self).filter(query7.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query8.className, predicate: query8.predicate)!, + realm.objects(T8.self).filter(query8.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query9.className, predicate: query9.predicate)!, + realm.objects(T9.self).filter(query9.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query10.className, predicate: query10.predicate)!, + realm.objects(T10.self).filter(query10.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query11.className, predicate: query11.predicate)!, + realm.objects(T11.self).filter(query11.predicate))) } - private func nextIndex(for index: Int?) -> Int? { - if let index = index, index < self.rlmSubscriptionSet.count - 1 { - return index + 1 - } - return nil + /** + Appends the query to the current subscription set and wait for the server to acknowledge the subscription, + returns a tuple of `QueryResults`s containing all the data associated to this queries. + + - parameter query: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query2: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query3: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query4: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query5: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query6: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query7: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query8: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query9: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query10: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query11: A `QuerySubscription` representing the query which will be used for the subscription. + - parameter query12: A `QuerySubscription` representing the query which will be used for the subscription. + - returns: A tuple of `QueryResults`s for the given subscriptions containing the data for each query. + + - throws: An `NSError` if the subscription couldn't be completed by the client or server. + */ + // swiftlint:disable large_tuple + public func subscribe(to query: QuerySubscription, _ query2: QuerySubscription, _ query3: QuerySubscription, _ query4: QuerySubscription, _ query5: QuerySubscription, _ query6: QuerySubscription, _ query7: QuerySubscription, _ query8: QuerySubscription, _ query9: QuerySubscription, _ query10: QuerySubscription, _ query11: QuerySubscription, _ query12: QuerySubscription) async throws -> (QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults, QueryResults) { + try await subscribe(query, query2, query3, query4, query5, query6, query7, query8, query9, query10, query11, query12) + return (QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query.className, predicate: query.predicate)!, + realm.objects(T1.self).filter(query.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query2.className, predicate: query2.predicate)!, + realm.objects(T2.self).filter(query2.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query3.className, predicate: query3.predicate)!, + realm.objects(T3.self).filter(query3.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query4.className, predicate: query4.predicate)!, + realm.objects(T4.self).filter(query4.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query5.className, predicate: query5.predicate)!, + realm.objects(T5.self).filter(query5.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query6.className, predicate: query6.predicate)!, + realm.objects(T6.self).filter(query6.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query7.className, predicate: query7.predicate)!, + realm.objects(T7.self).filter(query7.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query8.className, predicate: query8.predicate)!, + realm.objects(T8.self).filter(query8.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query9.className, predicate: query9.predicate)!, + realm.objects(T9.self).filter(query9.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query10.className, predicate: query10.predicate)!, + realm.objects(T10.self).filter(query10.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query11.className, predicate: query11.predicate)!, + realm.objects(T11.self).filter(query11.predicate)), + QueryResults(rlmSyncSubscriptionSet.subscription(withClassName: query12.className, predicate: query12.predicate)!, + realm.objects(T12.self).filter(query12.predicate))) } - mutating public func next() -> RLMSyncSubscription? { - if let index = self.nextIndex(for: self.index) { - self.index = index - return rlmSubscriptionSet.object(at: UInt(index)) + /** + Removes all subscriptions from the subscription set. + */ + public func unsubscribeAll() async throws { + try await write { + rlmSyncSubscriptionSet.removeAllSubscriptions() } - return nil } -} -#if swift(>=5.6) && canImport(_Concurrency) -@available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) -extension SyncSubscriptionSet { /** - Asynchronously creates and commit a write transaction and updates the subscription set, - this will not wait for the server to acknowledge and see all the data associated with this - collection of subscription. - - - parameter block: The block containing the subscriptions transactions to perform. + Removes zero or none subscriptions of the given type from the subscription set. - - throws: An `NSError` if the transaction could not be completed successfully. - If `block` throws, the function throws the propagated `ErrorType` instead. + - parameter type: The type of the subscriptions to be removed. */ - @MainActor - public func write(_ block: (() -> Void)) async throws { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - write(block) { error in - if let error = error { - continuation.resume(throwing: error) - } else { - continuation.resume() - } - } + public func unsubscribeAll(ofType type: T.Type) async throws { + try await write { + rlmSyncSubscriptionSet.removeAllSubscriptions(withClassName: type.className()) } } } -#endif // swift(>=5.5) +#endif // canImport(_Concurrency) + +extension User { + public func realm(configuration: Realm.Configuration = Realm.Configuration()) throws -> Realm { + return try Realm(configuration: flexibleSyncConfiguration(configuration)) + } +}