From 95cabd6d7c4d686efbdb8dd510908541085fb9ce Mon Sep 17 00:00:00 2001 From: pjechris Date: Fri, 14 Mar 2025 00:22:37 +0100 Subject: [PATCH 1/2] [registry] replace ObjectKey with Identifier --- Sources/CohesionKit/Identifier.swift | 17 +++++++ .../Observer/ObserverRegistry.swift | 45 +++++++------------ 2 files changed, 32 insertions(+), 30 deletions(-) create mode 100644 Sources/CohesionKit/Identifier.swift diff --git a/Sources/CohesionKit/Identifier.swift b/Sources/CohesionKit/Identifier.swift new file mode 100644 index 0000000..af63409 --- /dev/null +++ b/Sources/CohesionKit/Identifier.swift @@ -0,0 +1,17 @@ +/// a unique identifier to observe an object +struct Identifier: Hashable, Sendable, ExpressibleByStringLiteral { + let identifier: String + + init(identifier: String) { + self.identifier = identifier + } + + init(stringLiteral value: StringLiteralType) { + self.init(identifier: value) + } + + /// Generates an identifier for a node + init(node: EntityNode) { + self.init(identifier: "\(T.self)-\(node.hashValue)") + } +} diff --git a/Sources/CohesionKit/Observer/ObserverRegistry.swift b/Sources/CohesionKit/Observer/ObserverRegistry.swift index 1ccb220..984b85e 100644 --- a/Sources/CohesionKit/Observer/ObserverRegistry.swift +++ b/Sources/CohesionKit/Observer/ObserverRegistry.swift @@ -1,41 +1,26 @@ import Foundation -/// a unique hash identifying an object -typealias ObjectKey = Int - -extension ObjectKey { - init(of type: T.Type, id: Any) { - let key = "\(type)-\(id)" - - self.init(key.hashValue) - } - - init(_ node: EntityNode) { - self.init(of: T.self, id: node.hashValue) - } -} - /// Registers observers associated to an ``EntityNode``. /// The registry will handle notifying observers when a node is marked as changed class ObserverRegistry { let queue: DispatchQueue /// registered observer handlers - private var handlers: [ObjectKey: Set] = [:] + private var handlers: [Identifier: Set] = [:] /// nodes waiting for notifiying their observes about changes - private var pendingChanges: [ObjectKey: AnyWeak] = [:] + private var pendingChanges: [Identifier: AnyWeak] = [:] init(queue: DispatchQueue? = nil) { self.queue = queue ?? DispatchQueue.main } func addObserver(node: EntityNode, initial: Bool = false, onChange: @escaping (T) -> Void) -> Subscription { - addObserver(node: node, key: ObjectKey(node), initial: initial, onChange: onChange) + addObserver(node: node, identifier: Identifier(node: node), initial: initial, onChange: onChange) } /// register an observer to observe changes on an entity node. Everytime `ObserverRegistry` is notified about changes /// to this node `onChange` will be called. - func addObserver(node: EntityNode, key: ObjectKey, initial: Bool = false, onChange: @escaping (T) -> Void) -> Subscription { + func addObserver(node: EntityNode, identifier: Identifier, initial: Bool = false, onChange: @escaping (T) -> Void) -> Subscription { let handler = Handler { onChange($0.value) } if initial { @@ -49,7 +34,7 @@ class ObserverRegistry { } } - return subscribeHandler(handler, for: node, key: key) + return subscribeHandler(handler, for: node, identifier: identifier) } /// Add an observer handler to multiple nodes. @@ -71,7 +56,7 @@ class ObserverRegistry { } } - let subscriptions = nodes.map { node in subscribeHandler(handler, for: node, key: ObjectKey(node)) } + let subscriptions = nodes.map { node in subscribeHandler(handler, for: node, identifier: Identifier(node: node)) } return Subscription { subscriptions.forEach { $0.unsubscribe() } @@ -80,19 +65,19 @@ class ObserverRegistry { /// Mark a node as changed. Observers won't be notified of the change until ``postChanges`` is called func enqueueChange(for node: EntityNode) { - enqueueChange(for: node, key: ObjectKey(node)) + enqueueChange(for: node, identifier: Identifier(node: node)) } - func enqueueChange(for node: EntityNode, key: ObjectKey) { - pendingChanges[key] = Weak(value: node) + func enqueueChange(for node: EntityNode, identifier: Identifier) { + pendingChanges[identifier] = Weak(value: node) } func hasPendingChange(for node: EntityNode) -> Bool { - hasPendingChange(for: ObjectKey(node)) + hasPendingChange(for: Identifier(node: node)) } - func hasPendingChange(for key: ObjectKey) -> Bool { - pendingChanges[key] != nil + func hasPendingChange(for identifier: Identifier) -> Bool { + pendingChanges[identifier] != nil } /// Notify observers of all queued changes. Once notified pending changes are cleared out. @@ -123,14 +108,14 @@ class ObserverRegistry { } } - private func subscribeHandler(_ handler: Handler, for node: EntityNode, key: ObjectKey) -> Subscription { - handlers[key, default: []].insert(handler) + private func subscribeHandler(_ handler: Handler, for node: EntityNode, identifier: Identifier) -> Subscription { + handlers[identifier, default: []].insert(handler) // subscription keeps a strong ref to node, avoiding it from being released somehow while suscription is running return Subscription { [node] in withExtendedLifetime(node) { } - self.handlers[key]?.remove(handler) + self.handlers[identifier]?.remove(handler) } } } From 8d5e9acb1184ef67f71f321d47cbae366a07d1fa Mon Sep 17 00:00:00 2001 From: pjechris Date: Fri, 14 Mar 2025 09:25:59 +0100 Subject: [PATCH 2/2] migrate storages and EntityNode to use Identifier --- Sources/CohesionKit/Identifier.swift | 18 ++++++++--- .../Observer/ObserverRegistry.swift | 8 ++--- .../CohesionKit/Storage/AliasStorage.swift | 18 ++++------- .../CohesionKit/Storage/EntitiesStorage.swift | 18 ++++------- Sources/CohesionKit/Storage/EntityNode.swift | 32 +++++++++---------- .../Storage/EntityNodeTests.swift | 4 +-- 6 files changed, 48 insertions(+), 50 deletions(-) diff --git a/Sources/CohesionKit/Identifier.swift b/Sources/CohesionKit/Identifier.swift index af63409..9d36d63 100644 --- a/Sources/CohesionKit/Identifier.swift +++ b/Sources/CohesionKit/Identifier.swift @@ -2,16 +2,24 @@ struct Identifier: Hashable, Sendable, ExpressibleByStringLiteral { let identifier: String - init(identifier: String) { + init(_ identifier: String) { self.identifier = identifier } init(stringLiteral value: StringLiteralType) { - self.init(identifier: value) + self.init(value) } - /// Generates an identifier for a node - init(node: EntityNode) { - self.init(identifier: "\(T.self)-\(node.hashValue)") + /// Generates an identifier for type T with key as key + init(for type: T.Type, key: Any) { + self.init("\(T.self):\(key)") + } + + init(for object: T) { + self.init(for: T.self, key: object.id) + } + + init(for type: T.Type, key: AliasKey) { + self.init("alias:\(T.self):\(key.name)") } } diff --git a/Sources/CohesionKit/Observer/ObserverRegistry.swift b/Sources/CohesionKit/Observer/ObserverRegistry.swift index 984b85e..b77119f 100644 --- a/Sources/CohesionKit/Observer/ObserverRegistry.swift +++ b/Sources/CohesionKit/Observer/ObserverRegistry.swift @@ -15,7 +15,7 @@ class ObserverRegistry { } func addObserver(node: EntityNode, initial: Bool = false, onChange: @escaping (T) -> Void) -> Subscription { - addObserver(node: node, identifier: Identifier(node: node), initial: initial, onChange: onChange) + addObserver(node: node, identifier: node.id, initial: initial, onChange: onChange) } /// register an observer to observe changes on an entity node. Everytime `ObserverRegistry` is notified about changes @@ -56,7 +56,7 @@ class ObserverRegistry { } } - let subscriptions = nodes.map { node in subscribeHandler(handler, for: node, identifier: Identifier(node: node)) } + let subscriptions = nodes.map { node in subscribeHandler(handler, for: node, identifier: node.id) } return Subscription { subscriptions.forEach { $0.unsubscribe() } @@ -65,7 +65,7 @@ class ObserverRegistry { /// Mark a node as changed. Observers won't be notified of the change until ``postChanges`` is called func enqueueChange(for node: EntityNode) { - enqueueChange(for: node, identifier: Identifier(node: node)) + enqueueChange(for: node, identifier: node.id) } func enqueueChange(for node: EntityNode, identifier: Identifier) { @@ -73,7 +73,7 @@ class ObserverRegistry { } func hasPendingChange(for node: EntityNode) -> Bool { - hasPendingChange(for: Identifier(node: node)) + hasPendingChange(for: node.id) } func hasPendingChange(for identifier: Identifier) -> Bool { diff --git a/Sources/CohesionKit/Storage/AliasStorage.swift b/Sources/CohesionKit/Storage/AliasStorage.swift index 82a1a95..1339734 100644 --- a/Sources/CohesionKit/Storage/AliasStorage.swift +++ b/Sources/CohesionKit/Storage/AliasStorage.swift @@ -1,17 +1,17 @@ /// Keep a strong reference on each aliased node -typealias AliasStorage = [String: any AnyEntityNode] +typealias AliasStorage = [Identifier: any AnyEntityNode] extension AliasStorage { subscript(_ aliasKey: AliasKey) -> EntityNode>? { - get { self[buildKey(for: T.self, key: aliasKey)] as? EntityNode> } - set { self[buildKey(for: T.self, key: aliasKey)] = newValue } + get { self[Identifier(for: T.self, key: aliasKey)] as? EntityNode> } + set { self[Identifier(for: T.self, key: aliasKey)] = newValue } } subscript(safe key: AliasKey) -> EntityNode> { - mutating get { - let storeKey = buildKey(for: T.self, key: key) - return self[key: key, default: EntityNode(AliasContainer(key: key), key: storeKey, modifiedAt: nil)] - } + mutating get { + let storeKey = Identifier(for: T.self, key: key) + return self[key: key, default: EntityNode(AliasContainer(key: key), id: storeKey, modifiedAt: nil)] + } } subscript(key key: AliasKey, default defaultValue: @autoclosure () -> EntityNode>) @@ -28,8 +28,4 @@ extension AliasStorage { return node } } - - private func buildKey(for type: T.Type, key: AliasKey) -> String { - "\(type):\(key.name)" - } } diff --git a/Sources/CohesionKit/Storage/EntitiesStorage.swift b/Sources/CohesionKit/Storage/EntitiesStorage.swift index 935ac24..dd04b90 100644 --- a/Sources/CohesionKit/Storage/EntitiesStorage.swift +++ b/Sources/CohesionKit/Storage/EntitiesStorage.swift @@ -5,8 +5,8 @@ import Foundation /// Storage keeps weak references to objects. //// This allows to release entities automatically if no one is using them anymore (freeing memory space) struct EntitiesStorage { - /// the storage indexer. Stored content is [String: Weak>] - private typealias Storage = [String: AnyWeak] + /// the storage indexer. Stored content is [Identifier: Weak>] + private typealias Storage = [Identifier: AnyWeak] private var indexes: Storage = [:] @@ -15,17 +15,13 @@ struct EntitiesStorage { } subscript(_ type: T.Type, id id: T.ID) -> EntityNode? { - get { (indexes[key(for: T.self, id: id)] as? Weak>)?.value } - set { indexes[key(for: T.self, id: id)] = Weak(value: newValue) } + get { (indexes[Identifier(for: T.self, key: id)] as? Weak>)?.value } + set { indexes[Identifier(for: T.self, key: id)] = Weak(value: newValue) } } - subscript(_ key: String) -> AnyWeak? { - get { indexes[key] } - set { indexes[key] = newValue } - } - - private func key(for type: T.Type, id: Any) -> String { - "\(type)-\(id)" + subscript(_ index: Identifier) -> AnyWeak? { + get { indexes[index] } + set { indexes[index] = newValue } } } diff --git a/Sources/CohesionKit/Storage/EntityNode.swift b/Sources/CohesionKit/Storage/EntityNode.swift index bb98628..9ea39b7 100644 --- a/Sources/CohesionKit/Storage/EntityNode.swift +++ b/Sources/CohesionKit/Storage/EntityNode.swift @@ -3,14 +3,12 @@ import Combine struct EntityMetadata { /// children this entity is referencing/using - // TODO: change key to a ObjectKey - var childrenRefs: [String: AnyKeyPath] = [:] + var childrenRefs: [Identifier: AnyKeyPath] = [:] /// parents referencing this entity. This means this entity should be listed inside its parents `EntityMetadata.childrenRefs` attribute - // TODO: Change value to ObjectKey - var parentsRefs: Set = [] + var parentsRefs: Set = [] /// alias referencing this entity - var aliasesRefs: Set = [] + var aliasesRefs: Set = [] /// number of observers var observersCount: Int = 0 @@ -24,9 +22,10 @@ struct EntityMetadata { protocol AnyEntityNode: AnyObject { associatedtype Value + /// a unique identifier that should represent this node + var id: Identifier { get } var value: Value { get } var metadata: EntityMetadata { get } - var storageKey: String { get } func nullify() -> Bool func removeParent(_ node: any AnyEntityNode) @@ -54,7 +53,7 @@ class EntityNode: AnyEntityNode { var applyChildrenChanges = true - let storageKey: String + let id: Identifier /// last time `value` was changed. Any subsequent change must have a higher value to be applied /// if nil ref has no stamp and any change will be accepted @@ -62,15 +61,14 @@ class EntityNode: AnyEntityNode { /// entity children private(set) var children: [PartialKeyPath: SubscribedChild] = [:] - init(_ entity: T, key: String, modifiedAt: Stamp?) { - self.value = entity - self.modifiedAt = modifiedAt - self.storageKey = key + init(_ entity: T, id: Identifier, modifiedAt: Stamp?) { + self.value = entity + self.modifiedAt = modifiedAt + self.id = id } convenience init(_ entity: T, modifiedAt: Stamp?) where T: Identifiable { - let key = "\(T.self)-\(entity.id)" - self.init(entity, key: key, modifiedAt: modifiedAt) + self.init(entity, id: Identifier(for: entity), modifiedAt: modifiedAt) } /// change the entity to a new value. If modifiedAt is nil or > to previous date update the value will be changed @@ -106,7 +104,7 @@ class EntityNode: AnyEntityNode { } func removeParent(_ node: any AnyEntityNode) { - metadata.parentsRefs.remove(node.storageKey) + metadata.parentsRefs.remove(node.id) } func updateEntityRelationship(_ child: U) { @@ -114,7 +112,7 @@ class EntityNode: AnyEntityNode { return } - guard let keyPath = metadata.childrenRefs[child.storageKey] else { + guard let keyPath = metadata.childrenRefs[child.id] else { return } @@ -158,8 +156,8 @@ class EntityNode: AnyEntityNode { identity keyPath: KeyPath, update: @escaping (inout T, Element) -> Void ) { - metadata.childrenRefs[childNode.storageKey] = keyPath - childNode.metadata.parentsRefs.insert(storageKey) + metadata.childrenRefs[childNode.id] = keyPath + childNode.metadata.parentsRefs.insert(id) childrenNodes.append(childNode) } } diff --git a/Tests/CohesionKitTests/Storage/EntityNodeTests.swift b/Tests/CohesionKitTests/Storage/EntityNodeTests.swift index f6a1262..f671916 100644 --- a/Tests/CohesionKitTests/Storage/EntityNodeTests.swift +++ b/Tests/CohesionKitTests/Storage/EntityNodeTests.swift @@ -73,7 +73,7 @@ class EntityNodeTests: XCTestCase { node.observeChild(childNode, for: \.singleNode) - XCTAssertTrue(childNode.metadata.parentsRefs.contains(node.storageKey)) + XCTAssertTrue(childNode.metadata.parentsRefs.contains(node.id)) } func test_observeChild_childrenMetadataIsUpdated() { @@ -81,7 +81,7 @@ class EntityNodeTests: XCTestCase { node.observeChild(childNode, for: \.singleNode) - XCTAssertTrue(node.metadata.childrenRefs.keys.contains(childNode.storageKey)) + XCTAssertTrue(node.metadata.childrenRefs.keys.contains(childNode.id)) } func test_updateEntityRelationship_childIsUpdated() throws {