diff --git a/Examples/KanbanApp/KanbanApp/Kanban/KanbanViewModel.swift b/Examples/KanbanApp/KanbanApp/Kanban/KanbanViewModel.swift index 1d519497..94b9d840 100644 --- a/Examples/KanbanApp/KanbanApp/Kanban/KanbanViewModel.swift +++ b/Examples/KanbanApp/KanbanApp/Kanban/KanbanViewModel.swift @@ -37,8 +37,7 @@ class KanbanViewModel: ObservableObject { private let document: Document init() { - self.client = Client(rpcAddress: RPCAddress(host: "localhost", port: 8080), - options: ClientOptions()) + self.client = Client(RPCAddress(host: "localhost", port: 8080)) let docKey = "KanbanViewModel-8" self.document = Document(key: docKey) diff --git a/Examples/TextEditorApp/TextEditorApp/TextEditor/TextViewModel.swift b/Examples/TextEditorApp/TextEditorApp/TextEditor/TextViewModel.swift index a31544ff..9c117b8f 100644 --- a/Examples/TextEditorApp/TextEditorApp/TextEditor/TextViewModel.swift +++ b/Examples/TextEditorApp/TextEditorApp/TextEditor/TextViewModel.swift @@ -34,8 +34,7 @@ class TextViewModel { self.operationSubject = operationSubject // create client with RPCAddress. - self.client = Client(rpcAddress: RPCAddress(host: "localhost", port: 8080), - options: ClientOptions()) + self.client = Client(RPCAddress(host: "localhost", port: 8080)) // create a document self.document = Document(key: "codemirror") @@ -141,12 +140,12 @@ class TextViewModel { } } - func pause() async { - try? await self.client.pauseRemoteChanges(self.document) + func pause() async throws { + try await self.client.changeSyncMode(self.document, .realtimePushOnly) } - func resume() async { - try? await self.client.resumeRemoteChanges(self.document) + func resume() async throws { + try await self.client.changeSyncMode(self.document, .realtime) } private func decodePresence(_ dictionary: Any?) -> T? { diff --git a/Examples/TextEditorApp/TextEditorApp/TextEditor/View/TextEditorViewController.swift b/Examples/TextEditorApp/TextEditorApp/TextEditor/View/TextEditorViewController.swift index c8dd23d1..429a8fe2 100644 --- a/Examples/TextEditorApp/TextEditorApp/TextEditor/View/TextEditorViewController.swift +++ b/Examples/TextEditorApp/TextEditorApp/TextEditor/View/TextEditorViewController.swift @@ -46,10 +46,14 @@ class TextEditorViewController: UIViewController { didSet { if oldValue != self.isCompositioning { Task { - if self.isCompositioning { - await self.model?.pause() - } else { - await self.model?.resume() + do { + if self.isCompositioning { + try await self.model?.pause() + } else { + try await self.model?.resume() + } + } catch { + assertionFailure() } } } diff --git a/Sources/Core/Attachment.swift b/Sources/Core/Attachment.swift new file mode 100644 index 00000000..6318c390 --- /dev/null +++ b/Sources/Core/Attachment.swift @@ -0,0 +1,49 @@ +/* + * Copyright 2024 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import GRPC + +class Attachment { + var doc: Document + var docID: String + var syncMode: SyncMode + var remoteChangeEventReceived: Bool + var remoteWatchStream: GRPCAsyncServerStreamingCall? + var watchLoopReconnectTimer: Timer? + + init(doc: Document, docID: String, syncMode: SyncMode, remoteChangeEventReceived: Bool, remoteWatchStream: GRPCAsyncServerStreamingCall? = nil, watchLoopReconnectTimer: Timer? = nil) { + self.doc = doc + self.docID = docID + self.syncMode = syncMode + self.remoteChangeEventReceived = remoteChangeEventReceived + self.remoteWatchStream = remoteWatchStream + self.watchLoopReconnectTimer = watchLoopReconnectTimer + } + + /** + * `needRealtimeSync` returns whether the document needs to be synced in real time. + */ + func needRealtimeSync() async -> Bool { + if self.syncMode == .realtimeSyncOff { + return false + } + + let hasLocalChanges = await doc.hasLocalChanges() + + return self.syncMode != .manual && (hasLocalChanges || self.remoteChangeEventReceived) + } +} diff --git a/Sources/Core/Client.swift b/Sources/Core/Client.swift index fe89d198..3c4e5326 100644 --- a/Sources/Core/Client.swift +++ b/Sources/Core/Client.swift @@ -53,39 +53,29 @@ enum StreamConnectionStatus { } /** - * `SyncMode` is the mode of synchronization. It is used to determine - * whether to push and pull changes in PushPullChanges API. + * `SyncMode` defines synchronization modes for the PushPullChanges API. */ public enum SyncMode { /** - * `PushPull` is the mode that pushes and pulls changes. + * `manual` mode indicates that changes are not automatically pushed or pulled. */ - case pushPull + case manual /** - * `PushOnly` is the mode that pushes changes only. + * `realtime` mode indicates that changes are automatically pushed and pulled. */ - case pushOnly -} + case realtime -class Attachment { - var doc: Document - var docID: String - var isRealtimeSync: Bool - var realtimeSyncMode: SyncMode - var remoteChangeEventReceived: Bool - var remoteWatchStream: GRPCAsyncServerStreamingCall? - var watchLoopReconnectTimer: Timer? - - init(doc: Document, docID: String, isRealtimeSync: Bool, realtimeSyncMode: SyncMode, remoteChangeEventReceived: Bool, remoteWatchStream: GRPCAsyncServerStreamingCall? = nil, watchLoopReconnectTimer: Timer? = nil) { - self.doc = doc - self.docID = docID - self.isRealtimeSync = isRealtimeSync - self.realtimeSyncMode = realtimeSyncMode - self.remoteChangeEventReceived = remoteChangeEventReceived - self.remoteWatchStream = remoteWatchStream - self.watchLoopReconnectTimer = watchLoopReconnectTimer - } + /** + * `realtimePushonly` mode indicates that only local changes are automatically pushed. + */ + case realtimePushOnly + + /** + * `realtimeSyncoff` mode indicates that changes are not automatically pushed or pulled, + * but the watch stream is kept active. + */ + case realtimeSyncOff } /** @@ -191,7 +181,7 @@ public actor Client { * @param rpcAddr - the address of the RPC server. * @param opts - the options of the client. */ - public init(rpcAddress: RPCAddress, options: ClientOptions) { + public init(_ rpcAddress: RPCAddress, _ options: ClientOptions = ClientOptions()) { self.key = options.key ?? UUID().uuidString self.status = .deactivated @@ -295,7 +285,7 @@ public actor Client { * the client will synchronize the given document. */ @discardableResult - public func attach(_ doc: Document, _ initialPresence: PresenceData = [:], _ isRealtimeSync: Bool = true) async throws -> Document { + public func attach(_ doc: Document, _ initialPresence: PresenceData = [:], _ syncMode: SyncMode = .realtime) async throws -> Document { guard self.isActive else { throw YorkieError.clientNotActive(message: "\(self.key) is not active") } @@ -335,15 +325,15 @@ public actor Client { await doc.setStatus(.attached) - self.attachmentMap[doc.getKey()] = Attachment(doc: doc, docID: result.documentID, isRealtimeSync: isRealtimeSync, realtimeSyncMode: .pushPull, remoteChangeEventReceived: false) - try self.runWatchLoop(docKey) - - Logger.info("[AD] c:\"\(self.key))\" attaches d:\"\(doc.getKey())\"") + self.attachmentMap[doc.getKey()] = Attachment(doc: doc, docID: result.documentID, syncMode: syncMode, remoteChangeEventReceived: false) - if isRealtimeSync { + if syncMode != .manual { + try self.runWatchLoop(docKey) try await self.waitForInitialization(semaphore, docKey) } + Logger.info("[AD] c:\"\(self.key))\" attaches d:\"\(doc.getKey())\"") + self.semaphoresForInitialzation.removeValue(forKey: docKey) return doc @@ -408,28 +398,6 @@ public actor Client { } } - /** - * `pause` pause the realtime syncronization of the given document. - */ - public func pause(_ doc: Document) throws { - guard self.isActive else { - throw YorkieError.clientNotActive(message: "\(self.key) is not active") - } - - try self.changeRealtimeSync(doc, false) - } - - /** - * `resume` resume the realtime syncronization of the given document. - */ - public func resume(_ doc: Document) throws { - guard self.isActive else { - throw YorkieError.clientNotActive(message: "\(self.key) is not active") - } - - try self.changeRealtimeSync(doc, true) - } - /** * `remove` mrevoes the given document. */ @@ -473,64 +441,43 @@ public actor Client { } /** - * `changeRealtimeSync` changes the synchronization mode of the given document. + * `changeSyncMode` changes the synchronization mode of the given document. */ - private func changeRealtimeSync(_ doc: Document, _ isRealtimeSync: Bool) throws { + @discardableResult + public func changeSyncMode(_ doc: Document, _ syncMode: SyncMode) throws -> Document { let docKey = doc.getKey() - guard self.attachmentMap[docKey] != nil else { + guard let attachment = self.attachmentMap[docKey] else { throw YorkieError.unexpected(message: "Can't find attachment by docKey! [\(docKey)]") } - self.attachmentMap[docKey]?.isRealtimeSync = isRealtimeSync - - if isRealtimeSync { - // NOTE(hackerwins): In manual mode, the client does not receive change events - // from the server. Therefore, we need to set `remoteChangeEventReceived` to true - // to sync the local and remote changes. This has limitations in that unnecessary - // syncs occur if the client and server do not have any changes. - self.attachmentMap[docKey]?.remoteChangeEventReceived = true - try self.runWatchLoop(docKey) - } else { - try self.stopWatchLoop(docKey) - } - } - - /** - * `pauseRemoteChanges` pauses the synchronization of remote changes, - * allowing only local changes to be applied. - */ - public func pauseRemoteChanges(_ doc: Document) throws { - guard self.isActive else { - throw YorkieError.clientNotActive(message: "\(self.key) is not active") + let prevSyncMode = attachment.syncMode + if prevSyncMode == syncMode { + return doc } - let docKey = doc.getKey() + self.attachmentMap[docKey]?.syncMode = syncMode - guard self.attachmentMap[docKey] != nil else { - throw YorkieError.documentNotAttached(message: "\(doc.getKey()) is not attached when \(#function).") + // realtime to manual + if syncMode == .manual { + try self.stopWatchLoop(docKey) + return doc } - self.attachmentMap[docKey]?.realtimeSyncMode = .pushOnly - } - - /** - * `resumeRemoteChanges` resumes the synchronization of remote changes, - * allowing both local and remote changes to be applied. - */ - public func resumeRemoteChanges(_ doc: Document) throws { - guard self.isActive else { - throw YorkieError.clientNotActive(message: "\(self.key) is not active") + if syncMode == .realtime { + // NOTE(hackerwins): In non-pushpull mode, the client does not receive change events + // from the server. Therefore, we need to set `remoteChangeEventReceived` to true + // to sync the local and remote changes. This has limitations in that unnecessary + // syncs occur if the client and server do not have any changes. + self.attachmentMap[docKey]?.remoteChangeEventReceived = true } - let docKey = doc.getKey() - - guard self.attachmentMap[docKey] != nil else { - throw YorkieError.documentNotAttached(message: "\(doc.getKey()) is not attached when \(#function).") + // manual to realtime + if prevSyncMode == .manual { + try self.runWatchLoop(docKey) } - self.attachmentMap[docKey]?.realtimeSyncMode = .pushPull - self.attachmentMap[docKey]?.remoteChangeEventReceived = true + return doc } /** @@ -539,7 +486,11 @@ public actor Client { * local documents. */ @discardableResult - public func sync(_ doc: Document? = nil, _ syncMode: SyncMode = .pushPull) async throws -> [Document] { + public func sync(_ doc: Document? = nil) async throws -> [Document] { + guard self.isActive else { + throw YorkieError.clientNotActive(message: "\(self.key) is not active") + } + var attachment: Attachment? if let doc { @@ -550,7 +501,7 @@ public actor Client { } do { - return try await self.performSyncInternal(false, attachment, syncMode) + return try await self.performSyncInternal(false, attachment) } catch { let event = DocumentSyncedEvent(value: .syncFailed) self.eventStream.send(event) @@ -577,7 +528,7 @@ public actor Client { } @discardableResult - private func performSyncInternal(_ isRealtimeSync: Bool, _ attachment: Attachment? = nil, _ syncMode: SyncMode = .pushPull) async throws -> [Document] { + private func performSyncInternal(_ isRealtimeSync: Bool, _ attachment: Attachment? = nil) async throws -> [Document] { await self.syncSemaphore.wait() defer { @@ -588,23 +539,19 @@ public actor Client { do { if isRealtimeSync { - for (key, attachment) in self.attachmentMap.filter({ $0.value.isRealtimeSync }) { - let docChanged = await attachment.doc.hasLocalChanges() - - if docChanged || attachment.remoteChangeEventReceived { - self.clearAttachmentRemoteChangeEventReceived(key) - result.append(attachment.doc) - try await self.syncInternal(attachment, attachment.realtimeSyncMode) - } + for (key, attachment) in self.attachmentMap where await attachment.needRealtimeSync() { + self.clearAttachmentRemoteChangeEventReceived(key) + result.append(attachment.doc) + try await self.syncInternal(attachment, attachment.syncMode) } } else { if let attachment { result.append(attachment.doc) - try await self.syncInternal(attachment, syncMode) + try await self.syncInternal(attachment, .realtime) } else { for (_, attachment) in self.attachmentMap { result.append(attachment.doc) - try await self.syncInternal(attachment, attachment.realtimeSyncMode) + try await self.syncInternal(attachment, attachment.syncMode) } } } @@ -649,7 +596,7 @@ public actor Client { return } - guard self.attachmentMap[docKey]?.isRealtimeSync ?? false, let docID = self.attachmentMap[docKey]?.docID else { + guard let docID = self.attachmentMap[docKey]?.docID else { Logger.debug("[WL] c:\"\(self.key)\" exit watch loop") return } @@ -809,7 +756,7 @@ public actor Client { pushPullRequest.changePack = Converter.toChangePack(pack: requestPack) pushPullRequest.documentID = attachment.docID - pushPullRequest.pushOnly = syncMode == .pushOnly + pushPullRequest.pushOnly = syncMode == .realtimePushOnly do { let docKey = doc.getKey() @@ -821,7 +768,7 @@ public actor Client { // NOTE(chacha912, hackerwins): If syncLoop already executed with // PushPull, ignore the response when the syncMode is PushOnly. - if responsePack.hasChanges(), attachment.realtimeSyncMode == .pushOnly { + if responsePack.hasChanges(), attachment.syncMode == .realtimePushOnly { return doc } diff --git a/Tests/Integration/ClientIntegrationTests.swift b/Tests/Integration/ClientIntegrationTests.swift index 8a0a4454..de3e1a15 100644 --- a/Tests/Integration/ClientIntegrationTests.swift +++ b/Tests/Integration/ClientIntegrationTests.swift @@ -23,525 +23,648 @@ final class ClientIntegrationTests: XCTestCase { let rpcAddress = RPCAddress(host: "localhost", port: 8080) + func test_can_be_activated_decativated() async throws { + var options = ClientOptions() + let clientKey = "\(self.description)-\(Date().description)" + options.key = clientKey + options.syncLoopDuration = 50 + options.reconnectStreamDelay = 1000 + + let clientWithKey = Client(self.rpcAddress, options) + + var boolResult = await clientWithKey.isActive + XCTAssertFalse(boolResult) + try await clientWithKey.activate() + boolResult = await clientWithKey.isActive + XCTAssertTrue(boolResult) + var key = clientWithKey.key + XCTAssertEqual(key, clientKey) + try await clientWithKey.deactivate() + boolResult = await clientWithKey.isActive + XCTAssertFalse(boolResult) + + let clientWithoutKey = Client(self.rpcAddress) + + boolResult = await clientWithoutKey.isActive + XCTAssertFalse(boolResult) + try await clientWithoutKey.activate() + boolResult = await clientWithoutKey.isActive + XCTAssertTrue(boolResult) + key = clientWithoutKey.key + XCTAssertEqual(key.count, 36) + try await clientWithoutKey.deactivate() + boolResult = await clientWithoutKey.isActive + XCTAssertFalse(boolResult) + } + func test_can_handle_sync() async throws { - let options = ClientOptions() - let docKey = "\(self.description)-\(Date().description)".toDocKey + try await withTwoClientsAndDocuments(self.description) { c1, d1, c2, d2 in + try await d1.update { root, _ in + root.k1 = "v1" + } - let c1 = Client(rpcAddress: self.rpcAddress, options: options) - let c2 = Client(rpcAddress: self.rpcAddress, options: options) + try await c1.sync() + try await c2.sync() - let d1 = Document(key: docKey) - try await d1.update { root, _ in - root.k1 = "" - root.k2 = "" - root.k3 = "" - } + var d1JSON = await d1.toSortedJSON() + var d2JSON = await d2.toSortedJSON() - let d2 = Document(key: docKey) - try await d1.update { root, _ in - root.k1 = "" - root.k2 = "" - root.k3 = "" - } + XCTAssertEqual(d1JSON, d2JSON) - try await c1.activate() - try await c2.activate() + try await d1.update { root, _ in + root.k2 = "v2" + } - try await c1.attach(d1, [:], false) - try await c2.attach(d2, [:], false) + try await c1.sync() + try await c2.sync() - try await d1.update { root, _ in - root.k1 = "v1" - } + d1JSON = await d1.toSortedJSON() + d2JSON = await d2.toSortedJSON() - try await c1.sync() - try await c2.sync() + XCTAssertEqual(d1JSON, d2JSON) - var result = await d2.getRoot().get(key: "k1") as? String - XCTAssert(result == "v1") + try await d1.update { root, _ in + root.k3 = "v3" + } - try await d1.update { root, _ in - root.k2 = "v2" + try await c1.sync() + try await c2.sync() + + d1JSON = await d1.toSortedJSON() + d2JSON = await d2.toSortedJSON() + + XCTAssertEqual(d1JSON, d2JSON) } + } - try await c1.sync() - try await c2.sync() + /* + func test_can_recover_from_temporary_disconnect_realtime_sync() async throws { + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) + try await c1.activate() + try await c2.activate() - result = await d2.getRoot().get(key: "k2") as? String - XCTAssert(result == "v2") + let docKey = "\(self.description)-\(Date().description)".toDocKey + let d1 = Document(key: docKey) + let d2 = Document(key: docKey) - try await d1.update { root, _ in - root.k3 = "v3" - } + try await c1.attach(d1) + try await c2.attach(d2) - try await c1.sync() - try await c2.sync() + let d1Exp = self.expectation(description: "D1 exp 1") + let d2Exp = self.expectation(description: "D2 exp 1") - result = await d2.getRoot().get(key: "k3") as? String - XCTAssert(result == "v3") + var d1EventCount = 0 + var d2EventCount = 0 - try await c1.detach(d1) - try await c2.detach(d2) + await d1.subscribe { event, _ in + d1EventCount += 1 - try await c1.deactivate() - try await c2.deactivate() - } + if event is RemoteChangeEvent { + d1Exp.fulfill() + } + } - func test_can_handle_sync_auto() async throws { - let options = ClientOptions() - let docKey = "\(self.description)-\(Date().description)".toDocKey + await d2.subscribe { event, _ in + d2EventCount += 1 - let c1 = Client(rpcAddress: self.rpcAddress, options: options) - let c2 = Client(rpcAddress: self.rpcAddress, options: options) + if event is LocalChangeEvent { + d2Exp.fulfill() + } + } - let d1 = Document(key: docKey) - try await d1.update { root, _ in - root.k1 = "" - root.k2 = "" - root.k3 = "" - } + // Normal Condition + try await d2.update { root, _ in + root.k1 = "undefined" + } - let d2 = Document(key: docKey) - try await d1.update { root, _ in - root.k1 = "" - root.k2 = "" - root.k3 = "" - } + await fulfillment(of: [d1Exp, d2Exp], timeout: 5) + + var d1JSON = await d1.toSortedJSON() + var d2JSON = await d2.toSortedJSON() + XCTAssertEqual(d1JSON, d2JSON) + } + */ + + func test_can_change_sync_mode_realtime_manual() async throws { + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) try await c1.activate() try await c2.activate() - try await c1.attach(d1) - try await c2.attach(d2) + let docKey = "\(self.description)-\(Date().description)".toDocKey + let d1 = Document(key: docKey) + let d2 = Document(key: docKey) + + // 01. c1 and c2 attach the doc with manual sync mode. + // c1 updates the doc, but c2 does't get until call sync manually. + try await c1.attach(d1, [:], .manual) + try await c2.attach(d2, [:], .manual) try await d1.update { root, _ in - root.k1 = "v1" - root.k2 = "v2" - root.k3 = "v3" + root.version = "v1" } - try await Task.sleep(nanoseconds: 1_500_000_000) + var d1JSON = await d1.toSortedJSON() + var d2JSON = await d2.toSortedJSON() + XCTAssertEqual(d1JSON, "{\"version\":\"v1\"}") + XCTAssertEqual(d2JSON, "{}") - var result = await d2.getRoot().get(key: "k1") as? String - XCTAssert(result == "v1") - result = await d2.getRoot().get(key: "k2") as? String - XCTAssert(result == "v2") - result = await d2.getRoot().get(key: "k3") as? String - XCTAssert(result == "v3") + try await c1.sync() + try await c2.sync() - try await d1.update { root, _ in - root.integer = Int32.max - root.long = Int64.max - root.double = Double.pi + d2JSON = await d2.toSortedJSON() + XCTAssertEqual(d2JSON, "{\"version\":\"v1\"}") + + // 02. c2 changes the sync mode to realtime sync mode. + let c2Exp1 = expectation(description: "C2 Exp") + + var cancellable = c2.eventStream.sink { event in + if event.type == .documentSynced { + c2Exp1.fulfill() + } } - try await Task.sleep(nanoseconds: 1_500_000_000) + try await c2.changeSyncMode(d2, .realtime) + await fulfillment(of: [c2Exp1], timeout: 5) // sync occurs when resuming + + cancellable.cancel() - let resultInteger = await d2.getRoot().get(key: "integer") as? Int32 - XCTAssert(resultInteger == Int32.max) - let resultLong = await d2.getRoot().get(key: "long") as? Int64 - XCTAssert(resultLong == Int64.max) - let resultDouble = await d2.getRoot().get(key: "double") as? Double - XCTAssert(resultDouble == Double.pi) + let c2Exp2 = expectation(description: "C2 Exp 2") - let curr = Date() + cancellable = c2.eventStream.sink { event in + if event.type == .documentSynced { + c2Exp2.fulfill() + } + } try await d1.update { root, _ in - root.true = true - root.false = false - root.date = curr + root.version = "v2" } - try await Task.sleep(nanoseconds: 1_500_000_000) + try await c1.sync() + + await fulfillment(of: [c2Exp2], timeout: 5) + + d1JSON = await d1.toSortedJSON() + d2JSON = await d2.toSortedJSON() + XCTAssertEqual(d1JSON, "{\"version\":\"v2\"}") + XCTAssertEqual(d2JSON, "{\"version\":\"v2\"}") - let resultTrue = await d2.getRoot().get(key: "true") as? Bool - XCTAssert(resultTrue == true) - let resultFalse = await d2.getRoot().get(key: "false") as? Bool - XCTAssert(resultFalse == false) - let resultDate = await d2.getRoot().get(key: "date") as? Date - XCTAssert(resultDate?.trimedLessThanMilliseconds == curr.trimedLessThanMilliseconds) + cancellable.cancel() - try await c1.detach(d1) - try await c2.detach(d2) + // 03. c2 changes the sync mode to manual sync mode again. + try await c2.changeSyncMode(d2, .manual) + try await d1.update { root, _ in + root.version = "v3" + } + d1JSON = await d1.toSortedJSON() + d2JSON = await d2.toSortedJSON() + XCTAssertEqual(d1JSON, "{\"version\":\"v3\"}") + XCTAssertEqual(d2JSON, "{\"version\":\"v2\"}") + + try await c1.sync() + try await c2.sync() + d2JSON = await d2.toSortedJSON() + XCTAssertEqual(d2JSON, "{\"version\":\"v3\"}") try await c1.deactivate() try await c2.deactivate() } - func test_stream_connection_evnts() async throws { - let docKey = "\(self.description)-\(Date().description)".toDocKey - - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - - var eventCount = 0 - - c1.eventStream.sink { event in - switch event { - case let event as StreamConnectionStatusChangedEvent: - eventCount += 1 - switch event.value { - case .connected: - XCTAssertEqual(eventCount, 1) - case .disconnected: - XCTAssertEqual(eventCount, 2) - } - default: - break - } - }.store(in: &self.cancellables) + // swiftlint: disable function_body_length + func test_can_change_sync_mode_in_realtime() async throws { + // | | Step1 | Step2 | Step3 | Step4 | + // | c1 | PushPull | PushOnly | SyncOff | PushPull | + // | c2 | PushPull | SyncOff | PushOnly | PushPull | + // | c3 | PushPull | PushPull | PushPull | PushPull | + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) + let c3 = Client(rpcAddress) try await c1.activate() + try await c2.activate() + try await c3.activate() + let docKey = "\(self.description)-\(Date().description)".toDocKey let d1 = Document(key: docKey) + let d2 = Document(key: docKey) + let d3 = Document(key: docKey) + // 01. c1, c2, c3 attach to the same document in realtime sync. try await c1.attach(d1) + try await c2.attach(d2) + try await c3.attach(d3) - try await c1.detach(d1) - try await c1.deactivate() + let expectedEvents1: [DocEventType] = [.localChange, .remoteChange, .remoteChange, .localChange, .localChange, .remoteChange, .remoteChange, .remoteChange, .remoteChange] + let expectedEvents2: [DocEventType] = [.localChange, .remoteChange, .remoteChange, .localChange, .localChange, .remoteChange, .remoteChange, .remoteChange, .remoteChange] + let expectedEvents3: [DocEventType] = [.localChange, .remoteChange, .remoteChange, .localChange, .remoteChange, .localChange, .remoteChange, .remoteChange, .remoteChange] + let d1Exp1 = expectation(description: "D1 Exp 1") + let d2Exp1 = expectation(description: "D2 Exp 1") + let d3Exp1 = expectation(description: "D3 Exp 1") + let d1Exp2 = expectation(description: "D1 Exp 2") + let d2Exp2 = expectation(description: "D2 Exp 2") + let d3Exp2 = expectation(description: "D3 Exp 2") + let d1Exp3 = expectation(description: "D1 Exp 3") + let d2Exp3 = expectation(description: "D2 Exp 3") + let d3Exp3 = expectation(description: "D3 Exp 3") + let d1Exp4 = expectation(description: "D1 Exp 4") + let d2Exp4 = expectation(description: "D2 Exp 4") + let d3Exp4 = expectation(description: "D3 Exp 4") - XCTAssertEqual(eventCount, 2) - } + var d1EventCount = 0 + var d2EventCount = 0 + var d3EventCount = 0 - func test_client_pause_resume() async throws { - let c1 = Client(rpcAddress: self.rpcAddress, options: ClientOptions()) + await d1.subscribe { event, _ in + XCTAssertEqual(event.type, expectedEvents1[d1EventCount]) + d1EventCount += 1 - try await c1.activate() + if d1EventCount == 3 { + d1Exp1.fulfill() + } + if d1EventCount == 4 { + d1Exp2.fulfill() + } + if d1EventCount == 5 { + d1Exp3.fulfill() + } + if d1EventCount == 9 { + d1Exp4.fulfill() + } + } - let docKey = "\(self.description)-\(Date().description)".toDocKey + await d2.subscribe { event, _ in + XCTAssertEqual(event.type, expectedEvents2[d2EventCount]) + d2EventCount += 1 - let d1 = Document(key: docKey) + if d2EventCount == 3 { + d2Exp1.fulfill() + } + if d2EventCount == 4 { + d2Exp2.fulfill() + } + if d2EventCount == 5 { + d2Exp3.fulfill() + } + if d2EventCount == 9 { + d2Exp4.fulfill() + } + } + + await d3.subscribe { event, _ in + XCTAssertEqual(event.type, expectedEvents3[d3EventCount]) + d3EventCount += 1 - var c1NumberOfEvents = 0 - let c1ExpectedValues = [ - StreamConnectionStatus.connected, - StreamConnectionStatus.disconnected, - StreamConnectionStatus.connected, - StreamConnectionStatus.disconnected - ] - - let exp = self.expectation(description: "exp") - - c1.eventStream.sink { event in - switch event { - case let event as StreamConnectionStatusChangedEvent: - XCTAssertEqual(event.value, c1ExpectedValues[c1NumberOfEvents]) - c1NumberOfEvents += 1 - - if c1NumberOfEvents == c1ExpectedValues.count { - exp.fulfill() - } - default: - break + if d3EventCount == 3 { + d3Exp1.fulfill() } - }.store(in: &self.cancellables) + if d3EventCount == 5 { + d3Exp2.fulfill() + } + if d3EventCount == 8 { + d3Exp3.fulfill() + } + if d3EventCount == 9 { + d3Exp4.fulfill() + } + } - try await c1.attach(d1) + // 02. [Step1] c1, c2, c3 sync in realtime. + try await d1.update { root, _ in + root.c1 = Int64(0) + } + try await d2.update { root, _ in + root.c2 = Int64(0) + } + try await d3.update { root, _ in + root.c3 = Int64(0) + } + + await fulfillment(of: [d1Exp1, d2Exp1, d3Exp1], timeout: 5) + + var d1JSON = await d1.toSortedJSON() + var d2JSON = await d2.toSortedJSON() + var d3JSON = await d3.toSortedJSON() + XCTAssertEqual(d1JSON, "{\"c1\":0,\"c2\":0,\"c3\":0}") + XCTAssertEqual(d2JSON, "{\"c1\":0,\"c2\":0,\"c3\":0}") + XCTAssertEqual(d3JSON, "{\"c1\":0,\"c2\":0,\"c3\":0}") - try await c1.pause(d1) + // 03. [Step2] c1 sync with push-only mode, c2 sync with sync-off mode. + // c3 can get the changes of c1 and c2, because c3 sync with push-pull mode. + try await c1.changeSyncMode(d1, .realtimePushOnly) + try await c2.changeSyncMode(d2, .realtimeSyncOff) + try await d1.update { root, _ in + root.c1 = Int64(1) + } + try await d2.update { root, _ in + root.c2 = Int64(1) + } + try await d3.update { root, _ in + root.c3 = Int64(1) + } - try await c1.resume(d1) + await fulfillment(of: [d1Exp2, d2Exp2, d3Exp2], timeout: 5) - try await c1.detach(d1) + d1JSON = await d1.toSortedJSON() + d2JSON = await d2.toSortedJSON() + d3JSON = await d3.toSortedJSON() + XCTAssertEqual(d1JSON, "{\"c1\":1,\"c2\":0,\"c3\":0}") + XCTAssertEqual(d2JSON, "{\"c1\":0,\"c2\":1,\"c3\":0}") + XCTAssertEqual(d3JSON, "{\"c1\":1,\"c2\":0,\"c3\":1}") - try await c1.deactivate() + // 04. [Step3] c1 sync with sync-off mode, c2 sync with push-only mode. + try await c1.changeSyncMode(d1, .realtimeSyncOff) + try await c2.changeSyncMode(d2, .realtimePushOnly) + try await d1.update { root, _ in + root.c1 = Int64(2) + } + try await d2.update { root, _ in + root.c2 = Int64(2) + } + try await d3.update { root, _ in + root.c3 = Int64(2) + } + + await fulfillment(of: [d1Exp3, d2Exp3, d3Exp3], timeout: 5) + + d1JSON = await d1.toSortedJSON() + d2JSON = await d2.toSortedJSON() + d3JSON = await d3.toSortedJSON() + XCTAssertEqual(d1JSON, "{\"c1\":2,\"c2\":0,\"c3\":0}") + XCTAssertEqual(d2JSON, "{\"c1\":0,\"c2\":2,\"c3\":0}") + XCTAssertEqual(d3JSON, "{\"c1\":1,\"c2\":2,\"c3\":2}") - await fulfillment(of: [exp], timeout: 10) + // 05. [Step4] c1 and c2 sync with push-pull mode. + try await c1.changeSyncMode(d1, .realtime) + try await c2.changeSyncMode(d2, .realtime) + + await fulfillment(of: [d1Exp4, d2Exp4, d3Exp4], timeout: 5) + + d1JSON = await d1.toSortedJSON() + d2JSON = await d2.toSortedJSON() + d3JSON = await d3.toSortedJSON() + XCTAssertEqual(d1JSON, "{\"c1\":2,\"c2\":2,\"c3\":2}") + XCTAssertEqual(d2JSON, "{\"c1\":2,\"c2\":2,\"c3\":2}") + XCTAssertEqual(d3JSON, "{\"c1\":2,\"c2\":2,\"c3\":2}") + + try await c1.deactivate() + try await c2.deactivate() + try await c3.deactivate() } - func test_should_apply_previous_changes_when_resuming_document() async throws { - let c1 = Client(rpcAddress: self.rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: self.rpcAddress, options: ClientOptions()) + // swiftlint: enable function_body_length + func test_should_apply_previous_changes_when_switching_to_realtime_sync() async throws { + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) try await c1.activate() try await c2.activate() let docKey = "\(self.description)-\(Date().description)".toDocKey - let d1 = Document(key: docKey) let d2 = Document(key: docKey) - let exps = [self.expectation(description: "Exp 1"), self.expectation(description: "Exp 2"), self.expectation(description: "Exp 3")] - var expCount = 0 - - c2.eventStream.sink { event in + let exp1 = expectation(description: "exp 1") + var cancellable = c2.eventStream.sink { event in if event.type == .documentSynced { - exps[safe: expCount]?.fulfill() - expCount += 1 + exp1.fulfill() } - }.store(in: &self.cancellables) + } // 01. c2 attach the doc with realtime sync mode at first. - try await c1.attach(d1, [:], false) + try await c1.attach(d1, [:], .manual) try await c2.attach(d2) + try await d1.update { root, _ in root.version = "v1" } + try await c1.sync() - await fulfillment(of: [exps[0]], timeout: 1.5) - var d1Doc = await d1.toSortedJSON() - var d2Doc = await d2.toSortedJSON() + var d1JSON = await d1.toSortedJSON() + XCTAssertEqual(d1JSON, "{\"version\":\"v1\"}") - XCTAssertEqual(d1Doc, "{\"version\":\"v1\"}") - XCTAssertEqual(d2Doc, "{\"version\":\"v1\"}") + await fulfillment(of: [exp1], timeout: 5) - // 02. c2 pauses realtime sync mode. So, c2 doesn't get the changes of c1 - try await c2.pause(d2) + var d2JSON = await d2.toSortedJSON() + XCTAssertEqual(d2JSON, "{\"version\":\"v1\"}") + + // 02. c2 is changed to manual sync. So, c2 doesn't get the changes of c1. + try await c2.changeSyncMode(d2, .manual) try await d1.update { root, _ in root.version = "v2" } try await c1.sync() + d1JSON = await d1.toSortedJSON() + d2JSON = await d2.toSortedJSON() + XCTAssertEqual(d1JSON, "{\"version\":\"v2\"}") + XCTAssertEqual(d2JSON, "{\"version\":\"v1\"}") - d1Doc = await d1.toSortedJSON() - d2Doc = await d2.toSortedJSON() + // 03. c2 is changed to realtime sync. + // c2 should be able to apply changes made to the document while c2 is not in realtime sync. + cancellable.cancel() - XCTAssertEqual(d1Doc, "{\"version\":\"v2\"}") - XCTAssertEqual(d2Doc, "{\"version\":\"v1\"}") + let exp2 = expectation(description: "exp 2") - // 03. c2 resumes realtime sync mode. - // c2 should be able to apply changes made to the document while c2 is not in realtime sync. - try await c2.resume(d2) - await fulfillment(of: [exps[1]], timeout: 1.5) + cancellable = c2.eventStream.sink { event in + if event.type == .documentSynced { + exp2.fulfill() + } + } + + try await c2.changeSyncMode(d2, .realtime) - d2Doc = await d2.toSortedJSON() - XCTAssertEqual(d2Doc, "{\"version\":\"v2\"}") + await fulfillment(of: [exp2], timeout: 5) + + d2JSON = await d2.toSortedJSON() + XCTAssertEqual(d2JSON, "{\"version\":\"v2\"}") // 04. c2 should automatically synchronize changes. + cancellable.cancel() + + let exp3 = expectation(description: "exp 3") + + cancellable = c2.eventStream.sink { event in + if event.type == .documentSynced { + exp3.fulfill() + } + } + try await d1.update { root, _ in root.version = "v3" } try await c1.sync() - await fulfillment(of: [exps[2]], timeout: 1.5) - d1Doc = await d1.toSortedJSON() - d2Doc = await d2.toSortedJSON() + await fulfillment(of: [exp3], timeout: 5) + + d1JSON = await d1.toSortedJSON() + d2JSON = await d2.toSortedJSON() + XCTAssertEqual(d1JSON, "{\"version\":\"v3\"}") + XCTAssertEqual(d2JSON, "{\"version\":\"v3\"}") - XCTAssertEqual(d1Doc, "{\"version\":\"v3\"}") - XCTAssertEqual(d2Doc, "{\"version\":\"v3\"}") + cancellable.cancel() try await c1.deactivate() try await c2.deactivate() } - func test_can_change_sync_mode_in_realtime_sync() async throws { - let c1 = Client(rpcAddress: self.rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: self.rpcAddress, options: ClientOptions()) - let c3 = Client(rpcAddress: self.rpcAddress, options: ClientOptions()) - + func test_should_not_include_changes_applied_in_push_only_mode_when_switching_to_realtime_sync() async throws { + let c1 = Client(rpcAddress) try await c1.activate() - try await c2.activate() - try await c3.activate() - - let docKey = "\(self.description)-\(Date().description)".toDocKey + let docKey = "\(Date().description)-\(self.description)".toDocKey let d1 = Document(key: docKey) - let d2 = Document(key: docKey) - let d3 = Document(key: docKey) - // 01. c1, c2, c3 attach to the same document in realtime sync. - try await c1.attach(d1) - try await c2.attach(d2) - try await c3.attach(d3) + try await c1.attach(d1, [:], .manual) - // 02. c1, c2 sync in realtime. + // 02. cli update the document with creating a counter + // and sync with push-pull mode: CP(1, 1) -> CP(2, 2) try await d1.update { root, _ in - root.c1 = Int64(0) + root.counter = JSONCounter(value: Int64(0)) } - try await d2.update { root, _ in - root.c2 = Int64(0) - } - - try await Task.sleep(nanoseconds: 1_500_000_000) - - var d1Doc = await d1.toSortedJSON() - var d2Doc = await d2.toSortedJSON() + var checkpoint = await d1.checkpoint + XCTAssertEqual(checkpoint.getClientSeq(), 1) + XCTAssertEqual(checkpoint.getServerSeq(), 1) - XCTAssertEqual(d1Doc, "{\"c1\":0,\"c2\":0}") - XCTAssertEqual(d2Doc, "{\"c1\":0,\"c2\":0}") - - // 03. c1 and c2 sync with push-only mode. So, the changes of c1 and c2 - // are not reflected to each other. - // But, c can get the changes of c1 and c2, because c3 sync with push-pull mode. - try await c1.pauseRemoteChanges(d1) - try await c2.pauseRemoteChanges(d2) - - try await d1.update { root, _ in - root.c1 = Int64(1) + try await c1.sync() + checkpoint = await d1.checkpoint + XCTAssertEqual(checkpoint.getClientSeq(), 2) + XCTAssertEqual(checkpoint.getServerSeq(), 2) + + // 03. cli update the document with increasing the counter(0 -> 1) + // and sync with push-only mode: CP(2, 2) -> CP(3, 2) + let exp1 = expectation(description: "exp 1") + let cancellable = c1.eventStream.sink { event in + if event.type == .documentSynced { + exp1.fulfill() + } } - try await d2.update { root, _ in - root.c2 = Int64(1) + try await d1.update { root, _ in + (root.counter as? JSONCounter)?.increase(value: 1) } - try await Task.sleep(nanoseconds: 1_500_000_000) - - d1Doc = await d1.toSortedJSON() - d2Doc = await d2.toSortedJSON() - let d3Doc = await d3.toSortedJSON() - - XCTAssertEqual(d1Doc, "{\"c1\":1,\"c2\":0}") - XCTAssertEqual(d2Doc, "{\"c1\":0,\"c2\":1}") - XCTAssertEqual(d3Doc, "{\"c1\":1,\"c2\":1}") + var changePack = await d1.createChangePack() + XCTAssertEqual(changePack.getChangeSize(), 1) - // 04. c1 and c2 sync with push-pull mode. - try await c1.resumeRemoteChanges(d1) - try await c2.resumeRemoteChanges(d2) + try await c1.changeSyncMode(d1, .realtimePushOnly) + await fulfillment(of: [exp1], timeout: 5) - try await Task.sleep(nanoseconds: 1_500_000_000) + cancellable.cancel() - d1Doc = await d1.toSortedJSON() - d2Doc = await d2.toSortedJSON() + checkpoint = await d1.checkpoint + XCTAssertEqual(checkpoint.getClientSeq(), 3) + XCTAssertEqual(checkpoint.getServerSeq(), 2) - XCTAssertEqual(d1Doc, "{\"c1\":1,\"c2\":1}") - XCTAssertEqual(d2Doc, "{\"c1\":1,\"c2\":1}") + try await c1.changeSyncMode(d1, .manual) - try await c1.deactivate() - try await c2.deactivate() - try await c3.deactivate() - } - - func test_can_be_built_from_a_snapshot() async throws { - let docKey = "\(self.description)-\(Date().description)".toDocKey - - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - - try await c1.activate() - try await c2.activate() - - let doc1 = Document(key: docKey) - try await c1.attach(doc1, [:], false) - let doc2 = Document(key: docKey) - try await c2.attach(doc2, [:], false) - - let snapshotThreshold = 500 - - try await doc1.update { root, _ in - root.tree = JSONTree(initialRoot: JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "abc")])) - root.text = JSONText() + // 04. cli update the document with increasing the counter(1 -> 2) + // and sync with push-pull mode. CP(3, 2) -> CP(4, 4) + try await d1.update { root, _ in + (root.counter as? JSONCounter)?.increase(value: 1) } - for index in 0 ..< snapshotThreshold { - try await doc1.update { root, _ in - try (root.tree as? JSONTree)?.edit(0, 0, JSONTreeTextNode(value: "\(index),")) - (root.text as? JSONText)?.edit(0, 0, "\(index),") - } - } + // The previous increase(0 -> 1) is already pushed to the server, + // so the ChangePack of the request only has the increase(1 -> 2). + changePack = await d1.createChangePack() + XCTAssertEqual(changePack.getChangeSize(), 1) try await c1.sync() - try await c2.sync() + checkpoint = await d1.checkpoint + XCTAssertEqual(checkpoint.getClientSeq(), 4) + XCTAssertEqual(checkpoint.getServerSeq(), 4) - let result1 = await doc1.toSortedJSON() - let result2 = await doc2.toSortedJSON() + let value = await(d1.getRoot().counter as? JSONCounter)?.value + XCTAssertEqual(value, 2) - XCTAssertEqual(result1, result2) + try await c1.deactivate() } func test_should_prevent_remote_changes_in_push_only_mode() async throws { - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) try await c1.activate() try await c2.activate() - let docKey = "\(self.description)-\(Date().description)".toDocKey + let docKey = "\(Date().description)-\(self.description)".toDocKey let d1 = Document(key: docKey) let d2 = Document(key: docKey) + + // 01. c2 attach the doc with realtime sync mode at first. try await c1.attach(d1) try await c2.attach(d2) - let expect1 = expectation(description: "d2 1") - let expect2 = expectation(description: "d1 3") - let expect3 = expectation(description: "wait task") + let exp1 = expectation(description: "exp 1") + let exp2 = expectation(description: "exp 2") + var eventCount1 = 0 - var d1EventCount = 0 - var d1lastEvent: DocEvent? await d1.subscribe { event, _ in - d1EventCount += 1 - d1lastEvent = event - - if d1EventCount == 3 { - expect2.fulfill() + eventCount1 += 1 + if event.type == .remoteChange, eventCount1 == 3 { + exp1.fulfill() } } - var d2EventCount = 0 - var d2lastEvent: DocEvent? await d2.subscribe { event, _ in - d2EventCount += 1 - d2lastEvent = event - - if d2EventCount == 1 { - expect1.fulfill() + if event.type == .remoteChange { + exp2.fulfill() } } try await d1.update { root, _ in - root.t = JSONTree(initialRoot: - JSONTreeElementNode(type: "doc", children: [ - JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "12")]), - JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "34")]) - ])) + root.tree = JSONTree(initialRoot: JSONTreeElementNode(type: "doc", children: [ + JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "12")]), + JSONTreeElementNode(type: "p", children: [JSONTreeTextNode(value: "34")]) + ])) } - await fulfillment(of: [expect1], timeout: 2) - XCTAssert(d2lastEvent is RemoteChangeEvent) + await fulfillment(of: [exp2], timeout: 5) - var d1XML = await(d1.getRoot().t as? JSONTree)?.toXML() - var d2XML = await(d2.getRoot().t as? JSONTree)?.toXML() + await d2.unsubscribe() - XCTAssertEqual(d1XML, /* html */ "

12

34

") - XCTAssertEqual(d2XML, /* html */ "

12

34

") + var d1JSON = await(d1.getRoot().tree as? JSONTree)?.toXML() + var d2JSON = await(d2.getRoot().tree as? JSONTree)?.toXML() + XCTAssertEqual(d1JSON, "

12

34

") + XCTAssertEqual(d2JSON, "

12

34

") try await d1.update { root, _ in - try (root.t as? JSONTree)?.edit(2, 2, JSONTreeTextNode(value: "a")) + try (root.tree as? JSONTree)?.edit(2, 2, JSONTreeTextNode(value: "a")) } - try await c1.sync() - // The Task at below will be performed when the pushPull API sent the request and wait a response in the syncInternal(). - Task { - // In push-only mode, remote-change events should not occur. - try await c2.pauseRemoteChanges(d2) - var remoteChangeOccured = false - - await d2.subscribe { event, _ in - if event.type == .remoteChange { - remoteChangeOccured = true - } - } - - try await Task.sleep(nanoseconds: 2_000_000_000) - await d2.unsubscribe() - XCTAssert(remoteChangeOccured == false) - - try await c2.resumeRemoteChanges(d2) - - expect3.fulfill() - } - // Simulate the situation in the runSyncLoop where a pushpull request has been sent // but a response has not yet been received. try await c2.sync() - await fulfillment(of: [expect3], timeout: 10) + // In push-only mode, remote-change events should not occur. + try await c2.changeSyncMode(d2, .realtimePushOnly) + var remoteChangeOccured = false - try await d2.update { root, _ in - try (root.t as? JSONTree)?.edit(2, 2, JSONTreeTextNode(value: "b")) + await d2.subscribe { event, _ in + if event.type == .remoteChange { + remoteChangeOccured = true + } } - await fulfillment(of: [expect2], timeout: 2) - XCTAssert(d1lastEvent is RemoteChangeEvent) + try await Task.sleep(nanoseconds: 1_000_000_000) - d1XML = await(d1.getRoot().t as? JSONTree)?.toXML() - d2XML = await(d2.getRoot().t as? JSONTree)?.toXML() + XCTAssertFalse(remoteChangeOccured) - XCTAssertEqual(d1XML, /* html */ "

1ba2

34

") - XCTAssertEqual(d2XML, /* html */ "

1ba2

34

") + try await c2.changeSyncMode(d2, .realtime) - await d1.unsubscribe() - await d2.unsubscribe() + try await d2.update { root, _ in + try (root.tree as? JSONTree)?.edit(2, 2, JSONTreeTextNode(value: "b")) + } + + await fulfillment(of: [exp1], timeout: 5) + + d1JSON = await(d1.getRoot().tree as? JSONTree)?.toXML() + d2JSON = await(d2.getRoot().tree as? JSONTree)?.toXML() + XCTAssertEqual(d1JSON, "

1ba2

34

") + XCTAssertEqual(d2JSON, "

1ba2

34

") try await c1.deactivate() try await c2.deactivate() diff --git a/Tests/Integration/ClientTests.swift b/Tests/Integration/ClientTests.swift deleted file mode 100644 index 04a342d9..00000000 --- a/Tests/Integration/ClientTests.swift +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright 2022 The Yorkie Authors. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import Combine -import XCTest -@testable import Yorkie - -class ClientTests: XCTestCase { - var cancellables = Set() - - func test_activate_and_deactivate_client_with_key() async throws { - let clientKey = "\(self.description)-\(Date().description)" - let rpcAddress = RPCAddress(host: "localhost", port: 8080) - - let options = ClientOptions(key: clientKey) - let target: Client - var status = ClientStatus.deactivated - - target = Client(rpcAddress: rpcAddress, options: options) - - target.eventStream.sink { event in - switch event { - case let event as StatusChangedEvent: - status = event.value - default: - break - } - }.store(in: &self.cancellables) - - do { - try await target.activate() - } catch { - XCTFail(error.localizedDescription) - } - - var isActive = await target.isActive - - XCTAssertTrue(isActive) - XCTAssertEqual(target.key, clientKey) - XCTAssert(status == .activated) - - do { - try await target.deactivate() - } catch { - XCTFail(error.localizedDescription) - } - - isActive = await target.isActive - - XCTAssertFalse(isActive) - XCTAssert(status == .deactivated) - } - - func test_activate_and_deactivate_client_without_key() async throws { - let rpcAddress = RPCAddress(host: "localhost", port: 8080) - - let options = ClientOptions() - let target: Client - var status = ClientStatus.deactivated - - target = Client(rpcAddress: rpcAddress, options: options) - - target.eventStream.sink { event in - switch event { - case let event as StatusChangedEvent: - status = event.value - default: - break - } - }.store(in: &self.cancellables) - - try await target.activate() - - var isActive = await target.isActive - - XCTAssertTrue(isActive) - XCTAssertFalse(target.key.isEmpty) - XCTAssert(status == .activated) - - try await target.deactivate() - - isActive = await target.isActive - - XCTAssertFalse(isActive) - XCTAssert(status == .deactivated) - } - - func test_attach_detach_document_with_key() async throws { - let clientId = UUID().uuidString - let rpcAddress = RPCAddress(host: "localhost", port: 8080) - - let options = ClientOptions(key: clientId) - let target: Client - var status = ClientStatus.deactivated - - target = Client(rpcAddress: rpcAddress, options: options) - - target.eventStream.sink { event in - print("#### \(event)") - switch event { - case let event as StatusChangedEvent: - status = event.value - default: - break - } - }.store(in: &self.cancellables) - - do { - try await target.activate() - } catch { - XCTFail(error.localizedDescription) - } - - var isActive = await target.isActive - - XCTAssertTrue(isActive) - XCTAssertEqual(target.key, clientId) - XCTAssert(status == .activated) - - let doc = Document(key: "doc1") - - do { - try await target.attach(doc) - } catch { - XCTFail(error.localizedDescription) - } - - do { - try await target.detach(doc) - } catch { - XCTFail(error.localizedDescription) - } - - do { - try await target.deactivate() - } catch { - XCTFail(error.localizedDescription) - } - - isActive = await target.isActive - - XCTAssertFalse(isActive) - XCTAssert(status == .deactivated) - } - - func test_sync_option_with_multiple_clients() async throws { - let rpcAddress = RPCAddress(host: "localhost", port: 8080) - let docKey = "\(self.description)-\(Date().description)".toDocKey - - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c3 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - - try await c1.activate() - try await c2.activate() - try await c3.activate() - - // 01. c1, c2, c3 attach to the same document. - let d1 = Document(key: docKey) - try await c1.attach(d1, [:], false) - let d2 = Document(key: docKey) - try await c2.attach(d2, [:], false) - let d3 = Document(key: docKey) - try await c3.attach(d3, [:], false) - - // 02. c1, c2 sync with push-pull mode. - try await d1.update { root, _ in - root.c1 = Int64(0) - } - - try await d2.update { root, _ in - root.c2 = Int64(0) - } - - try await c1.sync() - try await c2.sync() - try await c1.sync() - - var result1 = await d1.getRoot().debugDescription - var result2 = await d2.getRoot().debugDescription - - XCTAssertEqual(result1, result2) - - // 03. c1 and c2 sync with push-only mode. So, the changes of c1 and c2 - // are not reflected to each other. - // But, c3 can get the changes of c1 and c2, because c3 sync with pull-pull mode. - try await d1.update { root, _ in - root.c1 = Int64(1) - } - - try await d2.update { root, _ in - root.c2 = Int64(1) - } - - try await c1.sync(d1, .pushOnly) - try await c2.sync(d2, .pushOnly) - try await c3.sync() - - result1 = await d1.getRoot().debugDescription - result2 = await d2.getRoot().debugDescription - - XCTAssertNotEqual(result1, result2) - - let result3 = await d3.getRoot().debugDescription - - XCTAssertEqual(result3, "{\"c1\":1,\"c2\":1}") - - // 04. c1 and c2 sync with push-pull mode. - try await c1.sync() - try await c2.sync() - - result1 = await d1.getRoot().debugDescription - result2 = await d2.getRoot().debugDescription - - XCTAssertEqual(result1, result3) - XCTAssertEqual(result2, result3) - - try await c1.detach(d1) - try await c2.detach(d2) - try await c3.detach(d3) - - try await c1.deactivate() - try await c2.deactivate() - try await c3.deactivate() - } - - func test_sync_option_with_mixed_mode() async throws { - let rpcAddress = RPCAddress(host: "localhost", port: 8080) - let docKey = "\(self.description)-\(Date().description)".toDocKey - - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - - try await c1.activate() - - // 01. cli attach to the same document having counter. - let d1 = Document(key: docKey) - try await c1.attach(d1, [:], false) - - // 02. cli update the document with creating a counter - // and sync with push-pull mode: CP(1, 1) -> CP(2, 2) - try await d1.update { root, _ in - root.counter = JSONCounter(value: Int64(0)) - } - - var checkpoint = await d1.checkpoint - XCTAssertEqual(Checkpoint(serverSeq: 1, clientSeq: 1), checkpoint) - - try await c1.sync() - - checkpoint = await d1.checkpoint - XCTAssertEqual(Checkpoint(serverSeq: 2, clientSeq: 2), checkpoint) - - // 03. cli update the document with increasing the counter(0 -> 1) - // and sync with push-only mode: CP(2, 2) -> CP(2, 3) - try await d1.update { root, _ in - (root.counter as? JSONCounter)!.increase(value: 1) - } - - var changePack = await d1.createChangePack() - - XCTAssertEqual(changePack.getChanges().count, 1) - - try await c1.sync(d1, .pushOnly) - - checkpoint = await d1.checkpoint - XCTAssertEqual(Checkpoint(serverSeq: 2, clientSeq: 3), checkpoint) - - // 04. cli update the document with increasing the counter(1 -> 2) - // and sync with push-pull mode. CP(2, 3) -> CP(4, 4) - try await d1.update { root, _ in - (root.counter as? JSONCounter)!.increase(value: 1) - } - - // The previous increase(0 -> 1) is already pushed to the server, - // so the ChangePack of the request only has the increase(1 -> 2). - changePack = await d1.createChangePack() - - XCTAssertEqual(changePack.getChanges().count, 1) - - try await c1.sync() - - checkpoint = await d1.checkpoint - XCTAssertEqual(Checkpoint(serverSeq: 4, clientSeq: 4), checkpoint) - - let counter = await(d1.getRoot().get(key: "counter") as? JSONCounter)! - - XCTAssertEqual(2, counter.value) - } -} diff --git a/Tests/Integration/CounterIntegrationTests.swift b/Tests/Integration/CounterIntegrationTests.swift index 29636f01..cc23d043 100644 --- a/Tests/Integration/CounterIntegrationTests.swift +++ b/Tests/Integration/CounterIntegrationTests.swift @@ -59,11 +59,10 @@ final class CounterIntegrationTests: XCTestCase { } func test_can_sync_counter() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey - self.c1 = Client(rpcAddress: self.rpcAddress, options: options) - self.c2 = Client(rpcAddress: self.rpcAddress, options: options) + self.c1 = Client(self.rpcAddress) + self.c2 = Client(self.rpcAddress) self.d1 = Document(key: docKey) self.d2 = Document(key: docKey) @@ -71,8 +70,8 @@ final class CounterIntegrationTests: XCTestCase { try await self.c1.activate() try await self.c2.activate() - try await self.c1.attach(self.d1, [:], false) - try await self.c2.attach(self.d2, [:], false) + try await self.c1.attach(self.d1, [:], .manual) + try await self.c2.attach(self.d2, [:], .manual) try await self.d1.update { root, _ in root.age = JSONCounter(value: Int32(1)) @@ -112,11 +111,10 @@ final class CounterIntegrationTests: XCTestCase { } func test_can_sync_counter_with_array() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey - self.c1 = Client(rpcAddress: self.rpcAddress, options: options) - self.c2 = Client(rpcAddress: self.rpcAddress, options: options) + self.c1 = Client(self.rpcAddress) + self.c2 = Client(self.rpcAddress) self.d1 = Document(key: docKey) self.d2 = Document(key: docKey) @@ -124,8 +122,8 @@ final class CounterIntegrationTests: XCTestCase { try await self.c1.activate() try await self.c2.activate() - try await self.c1.attach(self.d1, [:], false) - try await self.c2.attach(self.d2, [:], false) + try await self.c1.attach(self.d1, [:], .manual) + try await self.c2.attach(self.d2, [:], .manual) try await self.d1.update { root, _ in root.counts = [JSONCounter(value: Int32(1))] diff --git a/Tests/Integration/DocumentIntegrationTests.swift b/Tests/Integration/DocumentIntegrationTests.swift index 6b6855b4..5de8ae65 100644 --- a/Tests/Integration/DocumentIntegrationTests.swift +++ b/Tests/Integration/DocumentIntegrationTests.swift @@ -33,7 +33,7 @@ final class DocumentIntegrationTests: XCTestCase { let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey - self.c1 = Client(rpcAddress: self.rpcAddress, options: options) + self.c1 = Client(self.rpcAddress, options) self.d1 = Document(key: docKey) @@ -96,11 +96,10 @@ final class DocumentIntegrationTests: XCTestCase { } func test_removed_document_creation() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey - self.c1 = Client(rpcAddress: self.rpcAddress, options: options) - self.c2 = Client(rpcAddress: self.rpcAddress, options: options) + self.c1 = Client(self.rpcAddress) + self.c2 = Client(self.rpcAddress) try await self.c1.activate() try await self.c2.activate() @@ -137,11 +136,10 @@ final class DocumentIntegrationTests: XCTestCase { } func test_removed_document_pushpull() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey - self.c1 = Client(rpcAddress: self.rpcAddress, options: options) - self.c2 = Client(rpcAddress: self.rpcAddress, options: options) + self.c1 = Client(self.rpcAddress) + self.c2 = Client(self.rpcAddress) try await self.c1.activate() try await self.c2.activate() @@ -187,11 +185,10 @@ final class DocumentIntegrationTests: XCTestCase { } func test_removed_document_detachment() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey - self.c1 = Client(rpcAddress: self.rpcAddress, options: options) - self.c2 = Client(rpcAddress: self.rpcAddress, options: options) + self.c1 = Client(self.rpcAddress) + self.c2 = Client(self.rpcAddress) try await self.c1.activate() try await self.c2.activate() @@ -232,11 +229,10 @@ final class DocumentIntegrationTests: XCTestCase { } func test_removed_document_removal() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey - self.c1 = Client(rpcAddress: self.rpcAddress, options: options) - self.c2 = Client(rpcAddress: self.rpcAddress, options: options) + self.c1 = Client(self.rpcAddress) + self.c2 = Client(self.rpcAddress) try await self.c1.activate() try await self.c2.activate() @@ -284,10 +280,9 @@ final class DocumentIntegrationTests: XCTestCase { // └───────────┘ └─────┘ // Detach PushPull func test_document_state_transition() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey - self.c1 = Client(rpcAddress: self.rpcAddress, options: options) + self.c1 = Client(self.rpcAddress) try await self.c1.activate() @@ -367,11 +362,10 @@ final class DocumentIntegrationTests: XCTestCase { } func test_specify_the_topic_to_subscribe_to() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey - self.c1 = Client(rpcAddress: self.rpcAddress, options: options) - self.c2 = Client(rpcAddress: self.rpcAddress, options: options) + self.c1 = Client(self.rpcAddress) + self.c2 = Client(self.rpcAddress) try await self.c1.activate() try await self.c2.activate() diff --git a/Tests/Integration/GCTests.swift b/Tests/Integration/GCTests.swift index fbf426f5..2a91379d 100644 --- a/Tests/Integration/GCTests.swift +++ b/Tests/Integration/GCTests.swift @@ -143,27 +143,26 @@ class GCTests: XCTestCase { } func test_getGarbageLength_should_return_the_actual_number_of_elements_garbage_collected() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey let doc1 = Document(key: docKey) let doc2 = Document(key: docKey) - let client1 = Client(rpcAddress: rpcAddress, options: options) - let client2 = Client(rpcAddress: rpcAddress, options: options) + let client1 = Client(rpcAddress) + let client2 = Client(rpcAddress) try await client1.activate() try await client2.activate() // 1. initial state - try await client1.attach(doc1, [:], false) + try await client1.attach(doc1, [:], .manual) try await doc1.update { root, _ in root.point = ["x": Int64(0), "y": Int64(0)] } try await client1.sync() - try await client2.attach(doc2, [:], false) + try await client2.attach(doc2, [:], .manual) // 2. client1 updates doc try await doc1.update { root, _ in @@ -435,20 +434,19 @@ class GCTests: XCTestCase { } func test_can_handle_tree_garbage_collection_for_multi_client() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey let doc1 = Document(key: docKey) let doc2 = Document(key: docKey) - let client1 = Client(rpcAddress: rpcAddress, options: options) - let client2 = Client(rpcAddress: rpcAddress, options: options) + let client1 = Client(rpcAddress) + let client2 = Client(rpcAddress) try await client1.activate() try await client2.activate() - try await client1.attach(doc1, [:], false) - try await client2.attach(doc2, [:], false) + try await client1.attach(doc1, [:], .manual) + try await client2.attach(doc2, [:], .manual) try await doc1.update { root, _ in root.t = JSONTree(initialRoot: @@ -542,20 +540,19 @@ class GCTests: XCTestCase { } func test_can_handle_garbage_collection_for_container_type() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey let doc1 = Document(key: docKey) let doc2 = Document(key: docKey) - let client1 = Client(rpcAddress: rpcAddress, options: options) - let client2 = Client(rpcAddress: rpcAddress, options: options) + let client1 = Client(rpcAddress) + let client2 = Client(rpcAddress) try await client1.activate() try await client2.activate() - try await client1.attach(doc1, [:], false) - try await client2.attach(doc2, [:], false) + try await client1.attach(doc1, [:], .manual) + try await client2.attach(doc2, [:], .manual) try await doc1.update({ root, _ in root["1"] = Int64(1) @@ -631,20 +628,19 @@ class GCTests: XCTestCase { } func test_can_handle_garbage_collection_for_text_type() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey let doc1 = Document(key: docKey) let doc2 = Document(key: docKey) - let client1 = Client(rpcAddress: rpcAddress, options: options) - let client2 = Client(rpcAddress: rpcAddress, options: options) + let client1 = Client(rpcAddress) + let client2 = Client(rpcAddress) try await client1.activate() try await client2.activate() - try await client1.attach(doc1, [:], false) - try await client2.attach(doc2, [:], false) + try await client1.attach(doc1, [:], .manual) + try await client2.attach(doc2, [:], .manual) try await doc1.update({ root, _ in root.text = JSONText() @@ -724,20 +720,19 @@ class GCTests: XCTestCase { } func test_can_handle_garbage_collection_with_detached_document_test() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey let doc1 = Document(key: docKey) let doc2 = Document(key: docKey) - let client1 = Client(rpcAddress: rpcAddress, options: options) - let client2 = Client(rpcAddress: rpcAddress, options: options) + let client1 = Client(rpcAddress) + let client2 = Client(rpcAddress) try await client1.activate() try await client2.activate() - try await client1.attach(doc1, [:], false) - try await client2.attach(doc2, [:], false) + try await client1.attach(doc1, [:], .manual) + try await client2.attach(doc2, [:], .manual) try await doc1.update({ root, _ in root["1"] = Int64(1) @@ -801,16 +796,15 @@ class GCTests: XCTestCase { } func test_can_collect_removed_elements_from_both_root_and_clone() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey let doc = Document(key: docKey) - let client = Client(rpcAddress: rpcAddress, options: options) + let client = Client(rpcAddress) try await client.activate() - try await client.attach(doc, [:], false) + try await client.attach(doc, [:], .manual) try await doc.update { root, _ in root.point = ["x": Int64(0), "y": Int64(0)] @@ -831,19 +825,18 @@ class GCTests: XCTestCase { } func test_can_purges_removed_elements_after_peers_can_not_access_them() async throws { - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey let doc1 = Document(key: docKey) let doc2 = Document(key: docKey) - let client1 = Client(rpcAddress: rpcAddress, options: options) - let client2 = Client(rpcAddress: rpcAddress, options: options) + let client1 = Client(rpcAddress) + let client2 = Client(rpcAddress) try await client1.activate() try await client2.activate() - try await client1.attach(doc1, [:], false) + try await client1.attach(doc1, [:], .manual) try await doc1.update { root, _ in root.point = ["x": Int64(0), "y": Int64(0)] @@ -858,7 +851,7 @@ class GCTests: XCTestCase { try await client1.sync() - try await client2.attach(doc2, [:], false) + try await client2.attach(doc2, [:], .manual) len = await doc2.getGarbageLength() XCTAssertEqual(len, 1) diff --git a/Tests/Integration/IntegrationHelper.swift b/Tests/Integration/IntegrationHelper.swift index d2f16076..10a3f5ee 100644 --- a/Tests/Integration/IntegrationHelper.swift +++ b/Tests/Integration/IntegrationHelper.swift @@ -20,11 +20,10 @@ import XCTest func withTwoClientsAndDocuments(_ title: String, _ callback: (Client, Document, Client, Document) async throws -> Void) async throws { let rpcAddress = RPCAddress(host: "localhost", port: 8080) - let options = ClientOptions() let docKey = "\(Date().description)-\(title)".toDocKey - let c1 = Client(rpcAddress: rpcAddress, options: options) - let c2 = Client(rpcAddress: rpcAddress, options: options) + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) try await c1.activate() try await c2.activate() @@ -32,8 +31,8 @@ func withTwoClientsAndDocuments(_ title: String, _ callback: (Client, Document, let d1 = Document(key: docKey) let d2 = Document(key: docKey) - try await c1.attach(d1, [:], false) - try await c2.attach(d2, [:], false) + try await c1.attach(d1, [:], .manual) + try await c2.attach(d2, [:], .manual) try await callback(c1, d1, c2, d2) diff --git a/Tests/Integration/PresenceTests.swift b/Tests/Integration/PresenceTests.swift index eea815ef..eefd85f5 100644 --- a/Tests/Integration/PresenceTests.swift +++ b/Tests/Integration/PresenceTests.swift @@ -23,16 +23,16 @@ final class PresenceTests: XCTestCase { func test_can_be_built_from_a_snapshot() async throws { let docKey = "\(self.description)-\(Date().description)".toDocKey - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: rpcAddress, options: ClientOptions()) + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) try await c1.activate() try await c2.activate() let doc1 = Document(key: docKey) - try await c1.attach(doc1, [:], false) + try await c1.attach(doc1, [:], .manual) let doc2 = Document(key: docKey) - try await c2.attach(doc2, [:], false) + try await c2.attach(doc2, [:], .manual) let snapshotThreshold = 500 @@ -55,16 +55,16 @@ final class PresenceTests: XCTestCase { func test_can_be_set_initial_value_in_attach_and_be_removed_in_detach() async throws { let docKey = "\(self.description)-\(Date().description)".toDocKey - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: rpcAddress, options: ClientOptions()) + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) try await c1.activate() try await c2.activate() let doc1 = Document(key: docKey) - try await c1.attach(doc1, ["key": "key1"], false) + try await c1.attach(doc1, ["key": "key1"], .manual) let doc2 = Document(key: docKey) - try await c2.attach(doc2, ["key": "key2"], false) + try await c2.attach(doc2, ["key": "key2"], .manual) var presence = await doc1.getPresenceForTest(c1.id!)?["key"] as? String XCTAssertEqual(presence, "key1") @@ -89,16 +89,16 @@ final class PresenceTests: XCTestCase { func test_should_be_initialized_as_an_empty_object_if_no_initial_value_is_set_during_attach() async throws { let docKey = "\(self.description)-\(Date().description)".toDocKey - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: rpcAddress, options: ClientOptions()) + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) try await c1.activate() try await c2.activate() let doc1 = Document(key: docKey) - try await c1.attach(doc1, [:], false) + try await c1.attach(doc1, [:], .manual) let doc2 = Document(key: docKey) - try await c2.attach(doc2, [:], false) + try await c2.attach(doc2, [:], .manual) var presence = await doc1.getPresenceForTest(c1.id!) XCTAssertTrue(presence!.isEmpty) @@ -118,8 +118,8 @@ final class PresenceTests: XCTestCase { func test_can_be_updated_partially_by_doc_update_function() async throws { let docKey = "\(self.description)-\(Date().description)".toDocKey - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: rpcAddress, options: ClientOptions()) + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) try await c1.activate() try await c2.activate() @@ -146,9 +146,9 @@ final class PresenceTests: XCTestCase { } func test_should_return_only_online_clients() async throws { - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c3 = Client(rpcAddress: rpcAddress, options: ClientOptions()) + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) + let c3 = Client(rpcAddress) try await c1.activate() try await c2.activate() try await c3.activate() @@ -166,16 +166,16 @@ final class PresenceTests: XCTestCase { let doc1 = Document(key: docKey) try await c1.attach(doc1, ["name": "a1", "cursor": ["x": 0, "y": 0]]) - await doc1.subscribePresence { _, _ in + await doc1.subscribePresence { event, _ in eventCount1 += 1 - if eventCount1 == 1 { + if eventCount1 == 1, event.type == .watched { expect1.fulfill() } - if eventCount1 == 2 { + if eventCount1 == 2, event.type == .unwatched { expect2.fulfill() } - if eventCount1 == 3 { + if eventCount1 == 3, event.type == .watched { expect3.fulfill() } } @@ -184,7 +184,7 @@ final class PresenceTests: XCTestCase { let doc2 = Document(key: docKey) try await c2.attach(doc2, ["name": "b1", "cursor": ["x": 0, "y": 0]]) let doc3 = Document(key: docKey) - try await c3.attach(doc3, ["name": "c1", "cursor": ["x": 0, "y": 0]], false) + try await c3.attach(doc3, ["name": "c1", "cursor": ["x": 0, "y": 0]], .manual) await fulfillment(of: [expect1], timeout: 5) @@ -196,10 +196,10 @@ final class PresenceTests: XCTestCase { XCTAssert(resultPresences1.first { $0.clientID == c2ID }!.presence == doc2Presence) XCTAssert(resultPresences1.first { $0.clientID == c3ID } == nil) - // 02. c2 pauses the document (in manual sync), c3 resumes the document (in realtime sync). - try await c2.pause(doc2) + // 02. c2 is changed to manual sync, while c3 is changed to realtime sync. + try await c2.changeSyncMode(doc2, .manual) await fulfillment(of: [expect2], timeout: 5) // c2 unwatched - try await c3.resume(doc3) + try await c3.changeSyncMode(doc3, .realtime) await fulfillment(of: [expect3], timeout: 5) // c3 watched resultPresences1 = await doc1.getPresences() @@ -216,8 +216,8 @@ final class PresenceTests: XCTestCase { } func test_can_get_presence_value_using_p_get_within_doc_update_function() async throws { - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: rpcAddress, options: ClientOptions()) + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) try await c1.activate() try await c2.activate() let c1ID = await c1.id! @@ -225,10 +225,10 @@ final class PresenceTests: XCTestCase { let docKey = "\(self.description)-\(Date().description)".toDocKey let doc1 = Document(key: docKey) - try await c1.attach(doc1, ["counter": 0], false) + try await c1.attach(doc1, ["counter": 0], .manual) let doc2 = Document(key: docKey) - try await c2.attach(doc2, ["counter": 0], false) + try await c2.attach(doc2, ["counter": 0], .manual) try await doc1.update { _, presence in if let counter: Int = presence.get("counter") { @@ -319,8 +319,8 @@ final class PresenceSubscribeTests: XCTestCase { func test_should_be_synced_eventually() async throws { let docKey = "\(self.description)-\(Date().description)".toDocKey - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: rpcAddress, options: ClientOptions()) + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) try await c1.activate() try await c2.activate() let c1ID = await c1.id! @@ -404,8 +404,8 @@ final class PresenceSubscribeTests: XCTestCase { func test_should_receive_presence_changed_event_for_final_presence_if_there_are_multiple_presence_changes_within_doc_update() async throws { let docKey = "\(self.description)-\(Date().description)".toDocKey - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: rpcAddress, options: ClientOptions()) + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) try await c1.activate() try await c2.activate() let c1ID = await c1.id! @@ -475,8 +475,8 @@ final class PresenceSubscribeTests: XCTestCase { func test_can_receive_unwatched_event_when_a_client_detaches() async throws { let docKey = "\(self.description)-\(Date().description)".toDocKey - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: rpcAddress, options: ClientOptions()) + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) try await c1.activate() try await c2.activate() let c1ID = await c1.id! @@ -536,16 +536,16 @@ final class PresenceSubscribeTests: XCTestCase { } func test_can_receive_presence_related_event_only_when_using_realtime_sync() async throws { - let c1 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c2 = Client(rpcAddress: rpcAddress, options: ClientOptions()) - let c3 = Client(rpcAddress: rpcAddress, options: ClientOptions()) + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) + let c3 = Client(rpcAddress) try await c1.activate() try await c2.activate() try await c3.activate() let c2ID = await c2.id! let c3ID = await c3.id! - let docKey = "\(self.description)-\(Date().description)".toDocKey + let docKey = "\(Date().description)-\(self.description)".toDocKey var eventCount1 = 0 var eventReceived1 = [EventResult]() @@ -593,7 +593,7 @@ final class PresenceSubscribeTests: XCTestCase { let doc2 = Document(key: docKey) try await c2.attach(doc2, ["name": "b1", "cursor": ["x": 0, "y": 0]]) let doc3 = Document(key: docKey) - try await c3.attach(doc3, ["name": "c1", "cursor": ["x": 0, "y": 0]], false) + try await c3.attach(doc3, ["name": "c1", "cursor": ["x": 0, "y": 0]], .manual) await fulfillment(of: [expect1], timeout: 5) // c2 watched @@ -608,11 +608,11 @@ final class PresenceSubscribeTests: XCTestCase { await fulfillment(of: [expect2], timeout: 5) // c2 presence-changed - // 03. c2 pauses the document (in manual sync), c3 resumes the document (in realtime sync). + // 03. c2 is changed to manual sync, c3 resumes the document (in realtime sync). // c1 receives an unwatched event from c2 and a watched event from c3. - try await c2.pause(doc2) + try await c2.changeSyncMode(doc2, .manual) await fulfillment(of: [expect3], timeout: 5) // c2 unwatched - try await c3.resume(doc3) + try await c3.changeSyncMode(doc3, .realtime) await fulfillment(of: [expect5], timeout: 5) // c3 watched, c3 presence-changed // 04. c2 and c3 update the presence. @@ -626,9 +626,9 @@ final class PresenceSubscribeTests: XCTestCase { await fulfillment(of: [expect6], timeout: 5) // c3 presence-changed - // 05. c3 pauses the document (in manual sync), + // 05. c3 is changed to manual sync, // c1 receives an unwatched event from c3. - try await c3.pause(doc3) + try await c3.changeSyncMode(doc3, .manual) await fulfillment(of: [expect7], timeout: 5) // c3 unwatched // 06. c2 performs manual sync and then resumes(switches to realtime sync). @@ -638,7 +638,7 @@ final class PresenceSubscribeTests: XCTestCase { // We need to fix this issue. try await c2.sync() try await Task.sleep(nanoseconds: 1_500_000_000) - try await c2.resume(doc2) + try await c2.changeSyncMode(doc2, .realtime) await fulfillment(of: [expect8], timeout: 5) // c2 watched let result1 = [ diff --git a/Tests/Integration/TreeIntegrationTests.swift b/Tests/Integration/TreeIntegrationTests.swift index c144bc32..18dd2e88 100644 --- a/Tests/Integration/TreeIntegrationTests.swift +++ b/Tests/Integration/TreeIntegrationTests.swift @@ -1146,11 +1146,10 @@ final class TreeIntegrationStyleTests: XCTestCase { func test_can_handle_client_reload_case() async throws { let rpcAddress = RPCAddress(host: "localhost", port: 8080) - let options = ClientOptions() let docKey = "\(self.description)-\(Date().description)".toDocKey - let c1 = Client(rpcAddress: rpcAddress, options: options) - let c2 = Client(rpcAddress: rpcAddress, options: options) + let c1 = Client(rpcAddress) + let c2 = Client(rpcAddress) try await c1.activate() try await c2.activate() @@ -1158,8 +1157,8 @@ final class TreeIntegrationStyleTests: XCTestCase { let d1 = Document(key: docKey) let d2 = Document(key: docKey) - try await c1.attach(d1, [:], false) - try await c2.attach(d2, [:], false) + try await c1.attach(d1, [:], .manual) + try await c2.attach(d2, [:], .manual) // Perform a dummy update to apply changes up to the snapshot threshold. let snapshotThreshold = 500 @@ -1276,10 +1275,10 @@ final class TreeIntegrationStyleTests: XCTestCase { XCTAssertEqual(d2XML, /* html */ "

1 카카오2 네이3

") // A new client has been added. - let c3 = Client(rpcAddress: rpcAddress, options: options) + let c3 = Client(rpcAddress) try await c3.activate() let d3 = Document(key: docKey) - try await c3.attach(d3, [:], false) + try await c3.attach(d3, [:], .manual) var d3XML = await(d3.getRoot().t as? JSONTree)?.toXML()