Add client activate/deactivate (#7)

Co-authored-by: Youngteac Hong <susukang98@gmail.com>
This commit is contained in:
원형식 2022-09-14 21:21:13 +09:00 committed by GitHub
parent 08b5a3e412
commit d34fafccf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 453 additions and 5 deletions

View File

@ -4,7 +4,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "Yorkie", name: "Yorkie",
platforms: [.iOS(.v13)], platforms: [.iOS(.v13), .macOS(.v10_15)],
products: [ products: [
.library( .library(
name: "Yorkie", name: "Yorkie",

View File

@ -15,3 +15,317 @@
*/ */
import Foundation import Foundation
import GRPC
import NIO
/**
* `ClientStatus` represents the status of the client.
* @public
*/
enum ClientStatus: String {
/**
* Deactivated means that the client is not registered to the server.
*/
case deactivated
/**
* Activated means that the client is registered to the server.
* So, the client can sync documents with the server.
*/
case activated
}
/**
* `StreamConnectionStatus` is stream connection status types
* @public
*/
enum StreamConnectionStatus {
/**
* stream connected
*/
case connected
/**
* stream disconnected
*/
case disconnected
}
/**
* `DocumentSyncResultType` is document sync result types
* @public
*/
enum DocumentSyncResultType: String {
/**
* type when Document synced.
*/
case synced
/**
* type when Document sync failed.
*/
case syncFailed = "sync-failed"
}
/**
* `ClientEventType` is client event types
* @public
*/
enum ClientEventType: String {
/**
* client event type when status changed.
*/
case statusChanged = "status-changed"
/**
* client event type when documents changed.
*/
case documentsChanged = "documents-changed"
/**
* client event type when peers changed.
*/
case peersChanged = "peers-changed"
/**
* client event type when stream connection changed.
*/
case streamConnectionStatusChanged = "stream-connection-status-changed"
/**
* client event type when document synced.
*/
case documentSynced = "document-synced"
}
/**
* @internal
*/
protocol BaseClientEvent {
var type: ClientEventType { get }
}
/**
* `StatusChangedEvent` is an event that occurs when the Client's state changes.
*
* @public
*/
struct StatusChangedEvent: BaseClientEvent {
/**
* enum {@link ClientEventType}.StatusChanged
*/
var type: ClientEventType = .statusChanged
/**
* `DocumentsChangedEvent` value
*/
var value: ClientStatus
}
/**
* `DocumentsChangedEvent` is an event that occurs when documents attached to
* the client changes.
*
* @public
*/
struct DocumentsChangedEvent: BaseClientEvent {
/**
* enum {@link ClientEventType}.DocumentsChangedEvent
*/
var type: ClientEventType = .documentsChanged
/**
* `DocumentsChangedEvent` value
*/
var value: [String]
}
/**
* `StreamConnectionStatusChangedEvent` is an event that occurs when
* the client's stream connection state changes.
*
* @public
*/
struct StreamConnectionStatusChangedEvent: BaseClientEvent {
/**
* `StreamConnectionStatusChangedEvent` type
* enum {@link ClientEventType}.StreamConnectionStatusChangedEvent
*/
var type: ClientEventType = .streamConnectionStatusChanged
/**
* `StreamConnectionStatusChangedEvent` value
*/
var value: StreamConnectionStatus
}
/**
* `DocumentSyncedEvent` is an event that occurs when documents
* attached to the client are synced.
*
* @public
*/
struct DocumentSyncedEvent: BaseClientEvent {
/**
* `DocumentSyncedEvent` type
* enum {@link ClientEventType}.DocumentSyncedEvent
*/
var type: ClientEventType = .documentSynced
/**
* `DocumentSyncedEvent` value
*/
var value: DocumentSyncResultType
}
/**
* `PresenceInfo` is presence information of this client.
*
* @public
*/
struct PresenceInfo<P> {
var clock: Int
var data: P
}
/**
* `ClientOptions` are user-settable options used when defining clients.
*
* @public
*/
struct ClientOptions {
/**
* `key` is the client key. It is used to identify the client.
* If not set, a random key is generated.
*/
var key: String?
/**
* `apiKey` is the API key of the project. It is used to identify the project.
* If not set, API key of the default project is used.
*/
var apiKey: String?
/**
* `token` is the authentication token of this client. It is used to identify
* the user of the client.
*/
var token: String?
/**
* `syncLoopDuration` is the duration of the sync loop. After each sync loop,
* the client waits for the duration to next sync. The default value is
* `50`(ms).
*/
var syncLoopDuration: Int
/**
* `reconnectStreamDelay` is the delay of the reconnect stream. If the stream
* is disconnected, the client waits for the delay to reconnect the stream. The
* default value is `1000`(ms).
*/
var reconnectStreamDelay: Int
init(key: String? = nil, apiKey: String? = nil, token: String? = nil, syncLoopDuration: Int = 50, reconnectStreamDelay: Int = 1000) {
self.key = key
self.apiKey = apiKey
self.token = token
self.syncLoopDuration = syncLoopDuration
self.reconnectStreamDelay = reconnectStreamDelay
}
}
struct RPCAddress {
let host: String
let port: Int
}
/**
* `Client` is a normal client that can communicate with the server.
* It has documents and sends changes of the documents in local
* to the server to synchronize with other replicas in remote.
*
* @public
*/
final class Client {
private(set) var id: Data? // To be ActorID
let key: String
private(set) var status: ClientStatus
public var isActive: Bool {
self.status == .activated
}
private let syncLoopDuration: Int
private let reconnectStreamDelay: Int
private let rpcClient: Yorkie_V1_YorkieServiceAsyncClient
private let group: EventLoopGroup
/**
* @param rpcAddr - the address of the RPC server.
* @param opts - the options of the client.
*/
init(rpcAddress: RPCAddress, options: ClientOptions) throws {
self.key = options.key ?? UUID().uuidString
self.status = .deactivated
self.syncLoopDuration = options.syncLoopDuration
self.reconnectStreamDelay = options.reconnectStreamDelay
self.group = PlatformSupport.makeEventLoopGroup(loopCount: 1) // EventLoopGroup helpers
let channel: GRPCChannel
do {
channel = try GRPCChannelPool.with(target: .host(rpcAddress.host, port: rpcAddress.port),
transportSecurity: .plaintext,
eventLoopGroup: self.group)
} catch {
Logger.error("Failed to initialize client", error: error)
throw error
}
self.rpcClient = Yorkie_V1_YorkieServiceAsyncClient(channel: channel)
}
deinit {
try? self.group.syncShutdownGracefully()
}
/**
* `ativate` activates this client. That is, it register itself to the server
* and receives a unique ID from the server. The given ID is used to
* distinguish different clients.
*/
func activate() async throws {
guard self.isActive == false else {
return
}
var activateRequest = Yorkie_V1_ActivateClientRequest()
activateRequest.clientKey = self.key
let activateResponse: Yorkie_V1_ActivateClientResponse
do {
activateResponse = try await self.rpcClient.activateClient(activateRequest, callOptions: nil)
} catch {
Logger.error("Failed to request activate client(\(self.key)).", error: error)
throw error
}
self.id = activateResponse.clientID
self.status = .activated
Logger.debug("Client(\(self.key)) activated")
}
/**
* `deactivate` deactivates this client.
*/
func deactivate() async throws {
guard self.status == .activated, let clientId = self.id else {
return
}
var deactivateRequest = Yorkie_V1_DeactivateClientRequest()
deactivateRequest.clientID = clientId
do {
_ = try await self.rpcClient.deactivateClient(deactivateRequest)
} catch {
Logger.error("Failed to request deactivate client(\(self.key)).", error: error)
throw error
}
self.status = .deactivated
Logger.info("Client(\(self.key) deactivated.")
}
}

48
Sources/Core/Logger.swift Normal file
View File

@ -0,0 +1,48 @@
/*
* 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 Foundation
enum LogLevel: String {
case debug = "DEBUG"
case info = "INFO"
case warn = "WARN"
case error = "ERROR"
}
enum Logger {
static func debug(_ message: String, error: Error? = nil, filename: String = #file, function: String = #function, line: Int = #line) {
self.log(level: .debug, message, error: error, filename: filename, function: function, line: line)
}
static func info(_ message: String, error: Error? = nil, filename: String = #file, function: String = #function, line: Int = #line) {
self.log(level: .info, message, error: error, filename: filename, function: function, line: line)
}
static func warn(_ message: String, error: Error? = nil, filename: String = #file, function: String = #function, line: Int = #line) {
self.log(level: .warn, message, error: error, filename: filename, function: function, line: line)
}
static func error(_ message: String, error: Error? = nil, filename: String = #file, function: String = #function, line: Int = #line) {
self.log(level: .error, message, error: error, filename: filename, function: function, line: line)
}
static func log(level: LogLevel, _ message: String, error: Error? = nil, filename: String = #file, function: String = #function, line: Int = #line) {
let file = URL(fileURLWithPath: filename).lastPathComponent
let log = message + (error?.localizedDescription ?? "")
print("[\(level.rawValue)][\(file):\(line)] \(function) - \(log)")
}
}

View File

@ -0,0 +1,71 @@
/*
* 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 XCTest
@testable import Yorkie
class ClientTests: XCTestCase {
func test_activate_and_deactivate_client_with_key() async throws {
let clientId = UUID().uuidString
let rpcAddress = RPCAddress(host: "localhost", port: 8080)
let options = ClientOptions(key: clientId)
let target: Client
do {
target = try Client(rpcAddress: rpcAddress, options: options)
} catch {
XCTFail(error.localizedDescription)
return
}
do {
try await target.activate()
} catch {
XCTFail(error.localizedDescription)
}
XCTAssertTrue(target.isActive)
XCTAssertEqual(target.key, clientId)
do {
try await target.deactivate()
} catch {
XCTFail(error.localizedDescription)
}
XCTAssertFalse(target.isActive)
}
func test_activate_and_deactivate_client_without_key() async throws {
let rpcAddress = RPCAddress(host: "localhost", port: 8080)
let options = ClientOptions()
let target: Client
do {
target = try Client(rpcAddress: rpcAddress, options: options)
} catch {
XCTFail(error.localizedDescription)
return
}
try await target.activate()
XCTAssertTrue(target.isActive)
XCTAssertFalse(target.key.isEmpty)
try await target.deactivate()
XCTAssertFalse(target.isActive)
}
}

View File

@ -10,7 +10,9 @@
96DA808C28C5B7B400E2C1DA /* Yorkie.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96DA808228C5B7B400E2C1DA /* Yorkie.framework */; }; 96DA808C28C5B7B400E2C1DA /* Yorkie.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96DA808228C5B7B400E2C1DA /* Yorkie.framework */; };
96DA809128C5B7B400E2C1DA /* GRPCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96DA809028C5B7B400E2C1DA /* GRPCTests.swift */; }; 96DA809128C5B7B400E2C1DA /* GRPCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96DA809028C5B7B400E2C1DA /* GRPCTests.swift */; };
CE6071E528C5D7EE00A8783E /* CONTRIBUTING.md in Resources */ = {isa = PBXBuildFile; fileRef = CE6071E428C5D7EE00A8783E /* CONTRIBUTING.md */; }; CE6071E528C5D7EE00A8783E /* CONTRIBUTING.md in Resources */ = {isa = PBXBuildFile; fileRef = CE6071E428C5D7EE00A8783E /* CONTRIBUTING.md */; };
CE8C22ED28C9E7F200432DE5 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22EC28C9E7F200432DE5 /* Client.swift */; }; CE8C230528C9F1BD00432DE5 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C230428C9F1BD00432DE5 /* Client.swift */; };
CE8C230728D1514900432DE5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C230628D1514900432DE5 /* Logger.swift */; };
CE8C230B28D15FF200432DE5 /* ClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C230928D15F5A00432DE5 /* ClientTests.swift */; };
CE8C22EF28C9E85900432DE5 /* Change.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22EE28C9E85900432DE5 /* Change.swift */; }; CE8C22EF28C9E85900432DE5 /* Change.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22EE28C9E85900432DE5 /* Change.swift */; };
CE8C22F128C9E86A00432DE5 /* Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F028C9E86A00432DE5 /* Root.swift */; }; CE8C22F128C9E86A00432DE5 /* Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F028C9E86A00432DE5 /* Root.swift */; };
CE8C22F328C9E87800432DE5 /* Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F228C9E87800432DE5 /* Object.swift */; }; CE8C22F328C9E87800432DE5 /* Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F228C9E87800432DE5 /* Object.swift */; };
@ -42,7 +44,9 @@
96DA809028C5B7B400E2C1DA /* GRPCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRPCTests.swift; sourceTree = "<group>"; }; 96DA809028C5B7B400E2C1DA /* GRPCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRPCTests.swift; sourceTree = "<group>"; };
96DA809228C5B7B400E2C1DA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 96DA809228C5B7B400E2C1DA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
CE6071E428C5D7EE00A8783E /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = "<group>"; }; CE6071E428C5D7EE00A8783E /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = "<group>"; };
CE8C22EC28C9E7F200432DE5 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; }; CE8C230428C9F1BD00432DE5 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; };
CE8C230628D1514900432DE5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Logger.swift; path = Sources/Core/Logger.swift; sourceTree = SOURCE_ROOT; };
CE8C230928D15F5A00432DE5 /* ClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientTests.swift; sourceTree = "<group>"; };
CE8C22EE28C9E85900432DE5 /* Change.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Change.swift; sourceTree = "<group>"; }; CE8C22EE28C9E85900432DE5 /* Change.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Change.swift; sourceTree = "<group>"; };
CE8C22F028C9E86A00432DE5 /* Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Root.swift; sourceTree = "<group>"; }; CE8C22F028C9E86A00432DE5 /* Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Root.swift; sourceTree = "<group>"; };
CE8C22F228C9E87800432DE5 /* Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Object.swift; sourceTree = "<group>"; }; CE8C22F228C9E87800432DE5 /* Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Object.swift; sourceTree = "<group>"; };
@ -113,6 +117,7 @@
96DA808F28C5B7B400E2C1DA /* Tests */ = { 96DA808F28C5B7B400E2C1DA /* Tests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CE8C230828D15F5200432DE5 /* Core */,
CECCCB8128C96C8500544204 /* TestUtils */, CECCCB8128C96C8500544204 /* TestUtils */,
CE6071EB28C5ECA900A8783E /* API */, CE6071EB28C5ECA900A8783E /* API */,
96DA809228C5B7B400E2C1DA /* Info.plist */, 96DA809228C5B7B400E2C1DA /* Info.plist */,
@ -146,7 +151,7 @@
CE8C22E328C9E53400432DE5 /* Core */ = { CE8C22E328C9E53400432DE5 /* Core */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CE8C22EC28C9E7F200432DE5 /* Client.swift */, CE8C230428C9F1BD00432DE5 /* Client.swift */,
); );
path = Core; path = Core;
sourceTree = "<group>"; sourceTree = "<group>";
@ -171,6 +176,14 @@
path = Util; path = Util;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
CE8C230828D15F5200432DE5 /* Core */ = {
isa = PBXGroup;
children = (
CE8C230928D15F5A00432DE5 /* ClientTests.swift */,
);
path = Core;
sourceTree = "<group>";
};
CE8C22E628C9E55300432DE5 /* Change */ = { CE8C22E628C9E55300432DE5 /* Change */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -405,14 +418,15 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
CE8C230728D1514900432DE5 /* Logger.swift in Sources */,
CEEB17E428C84D26004988DD /* resources.pb.swift in Sources */, CEEB17E428C84D26004988DD /* resources.pb.swift in Sources */,
CE8C22F928C9E8CA00432DE5 /* Heap.swift in Sources */, CE8C22F928C9E8CA00432DE5 /* Heap.swift in Sources */,
CE8C22EF28C9E85900432DE5 /* Change.swift in Sources */, CE8C22EF28C9E85900432DE5 /* Change.swift in Sources */,
CEEB17E328C84D26004988DD /* yorkie.pb.swift in Sources */, CEEB17E328C84D26004988DD /* yorkie.pb.swift in Sources */,
CE8C230528C9F1BD00432DE5 /* Client.swift in Sources */,
CE8C22F528C9E88500432DE5 /* Operation.swift in Sources */, CE8C22F528C9E88500432DE5 /* Operation.swift in Sources */,
CE8C22F728C9E89100432DE5 /* Ticket.swift in Sources */, CE8C22F728C9E89100432DE5 /* Ticket.swift in Sources */,
CE8C22F328C9E87800432DE5 /* Object.swift in Sources */, CE8C22F328C9E87800432DE5 /* Object.swift in Sources */,
CE8C22ED28C9E7F200432DE5 /* Client.swift in Sources */,
CEEB17E528C84D26004988DD /* yorkie.grpc.swift in Sources */, CEEB17E528C84D26004988DD /* yorkie.grpc.swift in Sources */,
CE8C22F128C9E86A00432DE5 /* Root.swift in Sources */, CE8C22F128C9E86A00432DE5 /* Root.swift in Sources */,
); );
@ -423,6 +437,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
CECCCB8428C96CD600544204 /* XCTestCase+Extension.swift in Sources */, CECCCB8428C96CD600544204 /* XCTestCase+Extension.swift in Sources */,
CE8C230B28D15FF200432DE5 /* ClientTests.swift in Sources */,
96DA809128C5B7B400E2C1DA /* GRPCTests.swift in Sources */, 96DA809128C5B7B400E2C1DA /* GRPCTests.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;