Add client activate/deactivate (#7)
Co-authored-by: Youngteac Hong <susukang98@gmail.com>
This commit is contained in:
parent
08b5a3e412
commit
d34fafccf1
|
@ -4,7 +4,7 @@ import PackageDescription
|
|||
|
||||
let package = Package(
|
||||
name: "Yorkie",
|
||||
platforms: [.iOS(.v13)],
|
||||
platforms: [.iOS(.v13), .macOS(.v10_15)],
|
||||
products: [
|
||||
.library(
|
||||
name: "Yorkie",
|
||||
|
|
|
@ -15,3 +15,317 @@
|
|||
*/
|
||||
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -10,7 +10,9 @@
|
|||
96DA808C28C5B7B400E2C1DA /* Yorkie.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96DA808228C5B7B400E2C1DA /* Yorkie.framework */; };
|
||||
96DA809128C5B7B400E2C1DA /* GRPCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96DA809028C5B7B400E2C1DA /* GRPCTests.swift */; };
|
||||
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 */; };
|
||||
CE8C22F128C9E86A00432DE5 /* Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8C22F028C9E86A00432DE5 /* Root.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -113,6 +117,7 @@
|
|||
96DA808F28C5B7B400E2C1DA /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE8C230828D15F5200432DE5 /* Core */,
|
||||
CECCCB8128C96C8500544204 /* TestUtils */,
|
||||
CE6071EB28C5ECA900A8783E /* API */,
|
||||
96DA809228C5B7B400E2C1DA /* Info.plist */,
|
||||
|
@ -146,7 +151,7 @@
|
|||
CE8C22E328C9E53400432DE5 /* Core */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE8C22EC28C9E7F200432DE5 /* Client.swift */,
|
||||
CE8C230428C9F1BD00432DE5 /* Client.swift */,
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
|
@ -171,6 +176,14 @@
|
|||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE8C230828D15F5200432DE5 /* Core */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE8C230928D15F5A00432DE5 /* ClientTests.swift */,
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE8C22E628C9E55300432DE5 /* Change */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -405,14 +418,15 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE8C230728D1514900432DE5 /* Logger.swift in Sources */,
|
||||
CEEB17E428C84D26004988DD /* resources.pb.swift in Sources */,
|
||||
CE8C22F928C9E8CA00432DE5 /* Heap.swift in Sources */,
|
||||
CE8C22EF28C9E85900432DE5 /* Change.swift in Sources */,
|
||||
CEEB17E328C84D26004988DD /* yorkie.pb.swift in Sources */,
|
||||
CE8C230528C9F1BD00432DE5 /* Client.swift in Sources */,
|
||||
CE8C22F528C9E88500432DE5 /* Operation.swift in Sources */,
|
||||
CE8C22F728C9E89100432DE5 /* Ticket.swift in Sources */,
|
||||
CE8C22F328C9E87800432DE5 /* Object.swift in Sources */,
|
||||
CE8C22ED28C9E7F200432DE5 /* Client.swift in Sources */,
|
||||
CEEB17E528C84D26004988DD /* yorkie.grpc.swift in Sources */,
|
||||
CE8C22F128C9E86A00432DE5 /* Root.swift in Sources */,
|
||||
);
|
||||
|
@ -423,6 +437,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CECCCB8428C96CD600544204 /* XCTestCase+Extension.swift in Sources */,
|
||||
CE8C230B28D15FF200432DE5 /* ClientTests.swift in Sources */,
|
||||
96DA809128C5B7B400E2C1DA /* GRPCTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
Loading…
Reference in New Issue