Skip to content

Commit

Permalink
Fix presence to support JSONObject (#107)
Browse files Browse the repository at this point in the history
* Change the value of XXXChange to Change in Document.subscribe

* initial commit

* add Test codes.

* Expose pathToIndex API

* Bump up protobuf

* change yorkie server to 0.4.5

* Remove duplicated codes

* remove redundant codes

* Initial commit

* Correcting misported code

* Add TC for presence

* change xcode version to 14.3.1

* Update Tree.edit to allow insertion of multiple contents at once

* Replace selection with presence

* Rename TextRangeStruct to TextPosStructRange

* Clean up methods related to presence

* Add presence.get() to get presence value in doc.update()

* Change 'Documents' from plural to singular in DocEvent

* Cleanup proto

* Concurrent case handling for Yorkie.tree

* fix tree style TC

* Change TreeNode to have IDs instead of insPrev, insNext

* Remove select operation from text

* add more TCs for Tree

* Fix delete() in ElementRHT, Change func names

* Fix CRDTTree bugs

* Support multi-level and parital element selection

* Fix presence to support JSONObject

* add "guard" to improve readability

* add guard to improve readability

* correct typo. and remove forced unwrap

* apply review comment

* apply review comment

* correct typo

* Fix issues that do not cause delete operation events

* Change Tree Style to Codable

* lint codes

* Update swift-integration.yml

* Update swift-integration.yml

* Update swift-integration.yml

* Update GCTests.swift
  • Loading branch information
humdrum authored Aug 31, 2023
1 parent 3a30ed0 commit 10635c7
Show file tree
Hide file tree
Showing 26 changed files with 196 additions and 191 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/swift-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
xcode-version: '14.3.1'
- uses: actions/checkout@v3
- name: Setup Docker on macOS using Colima, Lima-VM, and Homebrew.
uses: 7hong13/setup-docker-macos-action@fix_formula_paths
uses: douglascamata/setup-docker-macos-action@fix-python-dep
id: docker
- run: docker-compose -f docker/docker-compose-ci.yml up --build -d
- name: Run tests
Expand Down
48 changes: 8 additions & 40 deletions Sources/API/Converter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,13 @@ extension Converter {
/**
* `toPresence` converts the given model to Protobuf format.
*/
static func toPresence(presence: PresenceData) -> PbPresence {
static func toPresence(presence: StringValueTypeDictionary) -> PbPresence {
var pbPresence = PbPresence()

for (key, value) in presence {
if value is AnyValueTypeDictionary || value is [Any],
let jsonObject = try? JSONSerialization.data(withJSONObject: value),
let jsonString = String(data: jsonObject, encoding: .utf8) {
pbPresence.data[key] = jsonString
} else if value is String {
pbPresence.data[key] = "\"\(value)\""
} else {
pbPresence.data[key] = "\(value)"
}
pbPresence.data[key] = value
}

return pbPresence
}

Expand All @@ -127,35 +119,11 @@ extension Converter {
/**
* `fromPresence` converts the given Protobuf format to model format.
*/
static func fromPresence(pbPresence: PbPresence) -> PresenceData {
var data = PresenceData()
static func fromPresence(pbPresence: PbPresence) -> StringValueTypeDictionary {
var data = StringValueTypeDictionary()

pbPresence.data.forEach { (key, value) in
if let dataValue = value.data(using: .utf8), let jsonValue = try? JSONSerialization.jsonObject(with: dataValue) as? AnyValueTypeDictionary {
data[key] = jsonValue
} else {
if value.first == "\"" && value.last == "\"" {
// Sring.
data[key] = value.substring(from: 1, to: value.count - 2)
} else if value.first == "[", value.last == "]",
let dataValue = "{\"\(key)\":\(value)}".data(using: .utf8), let jsonValue = try? JSONSerialization.jsonObject(with: dataValue) as? AnyValueTypeDictionary {
// Array. eg. "[\"a\", \"b\"]"
data.merge(jsonValue, uniquingKeysWith: { _, last in last })
} else {
if let intValue = Int(value) {
data[key] = intValue
} else if let doubleValue = Double(value) {
data[key] = doubleValue
} else if "\(true)" == value.lowercased() {
data[key] = true
} else if "\(false)" == value.lowercased() {
data[key] = false
} else {
data[key] = value
assertionFailure("Invalid Presence Value [\(key)]:[\(value)]")
}
}
}
data[key] = value
}

return data
Expand All @@ -178,7 +146,7 @@ extension Converter {
/**
* `fromPresences` converts the given Protobuf format to model format.
*/
static func fromPresences(_ pbPresences: [String: PbPresence]) -> [ActorID: PresenceData] {
static func fromPresences(_ pbPresences: [String: PbPresence]) -> [ActorID: StringValueTypeDictionary] {
pbPresences.mapValues { fromPresence(pbPresence: $0) }
}
}
Expand Down Expand Up @@ -1166,7 +1134,7 @@ extension Converter {
/**
* `bytesToSnapshot` creates a Snapshot from the given byte array.
*/
static func bytesToSnapshot(bytes: Data) throws -> (root: CRDTObject, presences: [ActorID: PresenceData]) {
static func bytesToSnapshot(bytes: Data) throws -> (root: CRDTObject, presences: [ActorID: StringValueTypeDictionary]) {
if bytes.isEmpty {
return (CRDTObject(createdAt: TimeTicket.initial), [:])
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Core/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -732,9 +732,9 @@ public actor Client {
// NOTE(chacha912): There is no presence, when PresenceChange(clear) is applied before unwatching.
// In that case, the 'unwatched' event is triggered while handling the PresenceChange.
let presence = await self.attachmentMap[docKey]?.doc.getPresence(publisher)

await self.attachmentMap[docKey]?.doc.removeOnlineClient(publisher)

if let presence {
await self.attachmentMap[docKey]?.doc.publishPresenceEvent(.unwatched, publisher, presence)
}
Expand Down
47 changes: 20 additions & 27 deletions Sources/Document/CRDT/CRDTTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ extension CRDTTreeNodeID {
/**
* `CRDTTreePosStruct` represents the structure of CRDTTreePos.
*/
struct CRDTTreePosStruct {
struct CRDTTreePosStruct: Codable {
let parentID: CRDTTreeNodeIDStruct
let leftSiblingID: CRDTTreeNodeIDStruct
}
Expand All @@ -196,7 +196,7 @@ struct CRDTTreePosStruct {
* `CRDTTreeNodeIDStruct` represents the structure of CRDTTreePos.
* It is used to serialize and deserialize the CRDTTreePos.
*/
struct CRDTTreeNodeIDStruct {
struct CRDTTreeNodeIDStruct: Codable {
let createdAt: TimeTicketStruct
let offset: Int32
}
Expand Down Expand Up @@ -323,17 +323,17 @@ final class CRDTTreeNode: IndexTreeNode {
* `clone` clones this node with the given offset.
*/
func clone(offset: Int32) -> CRDTTreeNode {
let clone = CRDTTreeNode(id: CRDTTreeNodeID(createdAt: self.id.createdAt, offset: offset), type: self.type, attributes: self.attrs)
let clone = CRDTTreeNode(id: CRDTTreeNodeID(createdAt: self.id.createdAt, offset: offset), type: self.type, attributes: self.attrs)
clone.removedAt = self.removedAt
clone.value = self.value
clone.size = self.size
clone.innerChildren = self.innerChildren.compactMap {
let childClone = $0.deepcopy()
childClone?.parent = clone

return childClone
}

return clone
}

Expand Down Expand Up @@ -432,7 +432,7 @@ final class CRDTTreeNode: IndexTreeNode {
}

return JSONTreeElementNode(type: self.type,
attributes: attrs.anyValueTypeDictionary,
attributes: attrs.toJSONObejct,
children: self.children.compactMap { $0.toJSONTreeNode })
}
}
Expand Down Expand Up @@ -460,7 +460,7 @@ class CRDTTree: CRDTGCElement {
self.nodeMapByID.put(node.id, node)
}
}

/**
* `findFloorNode` finds node of given id.
*/
Expand Down Expand Up @@ -518,7 +518,7 @@ class CRDTTree: CRDTGCElement {
if index <= parentNode.innerChildren.count {
for idx in index ..< parentNode.innerChildren.count {
let next = parentNode.innerChildren[idx]

if next.id.createdAt.after(editedAt) {
leftSiblingNode = next
} else {
Expand Down Expand Up @@ -614,30 +614,22 @@ class CRDTTree: CRDTGCElement {
if node.isText == false, contain != .all {
return
}

guard let actorID = node.createdAt.actorID else {
throw YorkieError.unexpected(message: "Can't get actorID")
}

let latestCreatedAt = latestCreatedAtMapByActor.isEmpty == false ? latestCreatedAtMapByActor[actorID] ?? TimeTicket.initial : TimeTicket.max

if node.canDelete(editedAt, latestCreatedAt) {
let latestCreatedAt = latestCreatedAtMap[actorID]
let createdAt = node.createdAt

if latestCreatedAt == nil || createdAt.after(latestCreatedAt!) {
latestCreatedAtMap[actorID] = createdAt
}

toBeRemoveds.append(node)
}
}

for node in toBeRemoveds {
node.remove(editedAt)

if node.isRemoved {
self.removedNodeMap[node.id.toIDString] = node
toBeRemoveds.append(node)
}
}

Expand Down Expand Up @@ -685,13 +677,14 @@ class CRDTTree: CRDTGCElement {
_ fromLeft: CRDTTreeNode,
_ toParent: CRDTTreeNode,
_ toLeft: CRDTTreeNode,
callback: @escaping (CRDTTreeNode, TagContained) throws -> Void) throws {
let fromIdx = try self.toIndex(fromParent, fromLeft)
let toIdx = try self.toIndex(toParent, toLeft)
callback: @escaping (CRDTTreeNode, TagContained) throws -> Void) throws
{
let fromIdx = try self.toIndex(fromParent, fromLeft)
let toIdx = try self.toIndex(toParent, toLeft)

return try self.indexTree.nodesBetween(fromIdx, toIdx, callback)
}

return try self.indexTree.nodesBetween(fromIdx, toIdx, callback)
}

/**
* `editByIndex` edits the given range with the given value.
* This method uses indexes instead of a pair of TreePos for testing.
Expand Down
2 changes: 1 addition & 1 deletion Sources/Document/CRDT/ElementRHT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ class ElementRHT {
* `get` returns the value of the given key.
*/
func get(key: String) -> CRDTElement? {
nodeMapByKey[key]?.value
self.nodeMapByKey[key]?.value
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/Document/CRDT/RGATreeSplit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protocol RGATreeSplitValue {
* `RGATreeSplitPosStruct` is a structure represents the meta data of the node pos.
* It is used to serialize and deserialize the node pos.
*/
public struct RGATreeSplitPosStruct {
public struct RGATreeSplitPosStruct: Codable {
let id: RGATreeSplitNodeIDStruct
let relativeOffset: Int32
}
Expand All @@ -49,7 +49,7 @@ public struct RGATreeSplitPosStruct {
* `RGATreeSplitNodeIDStruct` is a structure represents the meta data of the node id.
* It is used to serialize and deserialize the node id.
*/
public struct RGATreeSplitNodeIDStruct {
public struct RGATreeSplitNodeIDStruct: Codable {
let createdAt: TimeTicketStruct
let offset: Int32
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Document/Change/Change.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public struct Change {
* `execute` executes the operations of this change to the given root.
*/
@discardableResult
func execute(root: CRDTRoot, presences: inout [ActorID: PresenceData]) throws -> [any OperationInfo] {
func execute(root: CRDTRoot, presences: inout [ActorID: StringValueTypeDictionary]) throws -> [any OperationInfo] {
let opInfos = try self.operations.flatMap {
try $0.execute(root: root)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Document/DocEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public struct RemoteChangeEvent: ChangeEvent {
/**
* `PeersChangedValue` represents the value of the PeersChanged event.
*/
public typealias PeerElement = (clientID: ActorID, presence: PresenceData)
public typealias PeerElement = (clientID: ActorID, presence: [String: Any])

public struct InitializedEvent: DocEvent {
/**
Expand Down
28 changes: 14 additions & 14 deletions Sources/Document/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public actor Document {
private var localChanges: [Change]

private var root: CRDTRoot
private var clone: (root: CRDTRoot, presences: [ActorID: PresenceData])?
private var clone: (root: CRDTRoot, presences: [ActorID: StringValueTypeDictionary])?

private var defaultSubscribeCallback: SubscribeCallback?
private var subscribeCallbacks: [String: SubscribeCallback]
Expand All @@ -78,7 +78,7 @@ public actor Document {
/**
* `presences` is a map of client IDs to their presence information.
*/
private var presences: [ActorID: PresenceData]
private var presences: [ActorID: StringValueTypeDictionary]

public init(key: String) {
self.key = key
Expand Down Expand Up @@ -136,7 +136,7 @@ public actor Document {
self.publish(changeEvent)
}

if change.presenceChange != nil, let presence = self.presences[actorID] {
if change.presenceChange != nil, let presence = self.getPresence(actorID) {
self.publish(PresenceChangedEvent(value: (actorID, presence)))
}

Expand Down Expand Up @@ -384,9 +384,9 @@ public actor Document {
// their presence was initially absent, we can consider that we have
// received their initial presence, so trigger the 'watched' event
if self.onlineClients.contains(actorID) {
let peer = (actorID, presence)
let peer = (actorID, presence.toJSONObejct)

if self.presences[actorID] != nil {
if self.getPresence(actorID) != nil {
presenceEvent = PresenceChangedEvent(value: peer)
} else {
presenceEvent = WatchedEvent(value: peer)
Expand Down Expand Up @@ -487,7 +487,7 @@ public actor Document {
"[\(self.key)]"
}

func publishPresenceEvent(_ eventType: DocEventType, _ peerActorID: ActorID? = nil, _ presence: PresenceData? = nil) {
func publishPresenceEvent(_ eventType: DocEventType, _ peerActorID: ActorID? = nil, _ presence: [String: Any]? = nil) {
switch eventType {
case .initialized:
self.publish(InitializedEvent(value: self.getPresences()))
Expand Down Expand Up @@ -618,30 +618,30 @@ public actor Document {
/**
* `getMyPresence` returns the presence of the current client.
*/
public func getMyPresence() -> PresenceData? {
public func getMyPresence() -> [String: Any]? {
guard self.status == .attached, let id = self.changeID.getActorID() else {
return nil
}
return self.presences[id]

return self.presences[id]?.mapValues { $0.toJSONObject }
}

/**
* `getPresence` returns the presence of the given clientID.
*/
public func getPresence(_ clientID: ActorID) -> PresenceData? {
public func getPresence(_ clientID: ActorID) -> [String: Any]? {
guard self.onlineClients.contains(clientID) else {
return nil
}

return self.presences[clientID]
return self.presences[clientID]?.mapValues { $0.toJSONObject }
}

/**
* `getPresenceForTest` returns the presence of the given clientID.
*/
public func getPresenceForTest(_ clientID: ActorID) -> PresenceData? {
self.presences[clientID]
public func getPresenceForTest(_ clientID: ActorID) -> [String: Any]? {
self.presences[clientID]?.mapValues { $0.toJSONObject }
}

/**
Expand All @@ -651,7 +651,7 @@ public actor Document {
var presences = [PeerElement]()

for clientID in self.onlineClients {
if let presence = self.presences[clientID] {
if let presence = getPresence(clientID) {
presences.append((clientID, presence))
}
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/Document/Json/JSONTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ public class JSONTree {
/**
* `styleByPath` sets the attributes to the elements of the given path.
*/
public func styleByPath(_ path: [Int], _ attributes: [String: Any]) throws {
public func styleByPath(_ path: [Int], _ attributes: [String: Codable]) throws {
guard let context, let tree else {
throw YorkieError.unexpected(message: "it is not initialized yet")
}
Expand Down Expand Up @@ -271,7 +271,7 @@ public class JSONTree {
/**
* `style` sets the attributes to the elements of the given range.
*/
public func style(_ fromIdx: Int, _ toIdx: Int, _ attributes: [String: Any]) throws {
public func style(_ fromIdx: Int, _ toIdx: Int, _ attributes: [String: Codable]) throws {
guard let context, let tree else {
throw YorkieError.unexpected(message: "it is not initialized yet")
}
Expand Down Expand Up @@ -500,7 +500,7 @@ public class JSONTree {
* `TimeTicketStruct` is a structure represents the meta data of the ticket.
* It is used to serialize and deserialize the ticket.
*/
struct TimeTicketStruct {
struct TimeTicketStruct: Codable {
let lamport: String
let delimiter: UInt32
let actorID: ActorID?
Expand Down
Loading

0 comments on commit 10635c7

Please sign in to comment.