From a8c0177768567220c631bf3c8e06f26dcf310dae Mon Sep 17 00:00:00 2001 From: pjechris Date: Tue, 27 Aug 2024 08:49:09 +0200 Subject: [PATCH 1/9] add metadata type --- Sources/CohesionKit/Storage/EntityNode.swift | 22 +++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Sources/CohesionKit/Storage/EntityNode.swift b/Sources/CohesionKit/Storage/EntityNode.swift index ac77322..1d696a2 100644 --- a/Sources/CohesionKit/Storage/EntityNode.swift +++ b/Sources/CohesionKit/Storage/EntityNode.swift @@ -1,11 +1,31 @@ import Foundation import Combine +struct EntityMetadata { + /// children this entity is referencing/using + /// key: the children keypath in the parent, value: the key in EntitieStorage + // TODO: change value to a ObjectKey + var childrenRefs: [AnyKeyPath: String] = [:] + + /// parents referencing this entity. This means this entity should be listed inside its parents `EntityMetadata.childrenRefs` attribute + var parentsRefs: Set = [] + /// alias referencing this entity + var aliasesRefs: Set = [] + + /// number of observers + var observersCount: Int = 0 + + var isActivelyUsed: Bool { + observersCount > 0 || !parentsRefs.isEmpty || !aliasesRefs.isEmpty + } +} + /// Typed erased protocol protocol AnyEntityNode: AnyObject { var value: Any { get } + var metadata: EntityMetadata { get } - func nullify() + func nullify() } /// A graph node representing a entity of type `T` and its children. Anytime one of its children is updated the node From dfa9a081775dc5d4a7a6f12c5251d2b580319e19 Mon Sep 17 00:00:00 2001 From: pjechris Date: Tue, 27 Aug 2024 08:49:18 +0200 Subject: [PATCH 2/9] "store" EntityStorage key in EntityNode --- Sources/CohesionKit/Storage/EntityNode.swift | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Sources/CohesionKit/Storage/EntityNode.swift b/Sources/CohesionKit/Storage/EntityNode.swift index 1d696a2..4547f76 100644 --- a/Sources/CohesionKit/Storage/EntityNode.swift +++ b/Sources/CohesionKit/Storage/EntityNode.swift @@ -41,10 +41,14 @@ class EntityNode: AnyEntityNode { var value: Any { ref.value } + var metadata = EntityMetadata() + var applyChildrenChanges = true /// An observable entity reference let ref: Observable + let storageKey: String + private let onChange: ((EntityNode) -> Void)? /// last time the ref.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 @@ -52,14 +56,20 @@ class EntityNode: AnyEntityNode { /// entity children private(set) var children: [PartialKeyPath: SubscribedChild] = [:] - init(ref: Observable, modifiedAt: Stamp?, onChange: ((EntityNode) -> Void)? = nil) { + init(ref: Observable, key: String, modifiedAt: Stamp?, onChange: ((EntityNode) -> Void)? = nil) { self.ref = ref self.modifiedAt = modifiedAt self.onChange = onChange + self.storageKey = key + } + + convenience init(_ entity: T, key: String, modifiedAt: Stamp?, onChange: ((EntityNode) -> Void)? = nil) { + self.init(ref: Observable(value: entity), key: key, modifiedAt: modifiedAt, onChange: onChange) } - convenience init(_ entity: T, modifiedAt: Stamp?, onChange: ((EntityNode) -> Void)? = nil) { - self.init(ref: Observable(value: entity), modifiedAt: modifiedAt, onChange: onChange) + convenience init(_ entity: T, modifiedAt: Stamp?, onChange: ((EntityNode) -> Void)? = nil) where T: Identifiable { + let key = "\(T.self)-\(entity.id)" + self.init(entity, key: key, modifiedAt: modifiedAt, onChange: onChange) } /// change the entity to a new value. If modifiedAt is nil or > to previous date update the value will be changed From f21cdd7c16fb4de5e7aa77a3d34a02de283737d9 Mon Sep 17 00:00:00 2001 From: pjechris Date: Tue, 27 Aug 2024 09:20:24 +0200 Subject: [PATCH 3/9] =?UTF-8?q?update=20child=20and=20parent=20using=20Met?= =?UTF-8?q?adata=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/CohesionKit/EntityStore.swift | 16 ++++++++++ .../CohesionKit/Storage/EntitiesStorage.swift | 5 +++ Sources/CohesionKit/Storage/EntityNode.swift | 31 ++++++++++++++++--- .../Storage/EntityNodeTests.swift | 2 +- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/Sources/CohesionKit/EntityStore.swift b/Sources/CohesionKit/EntityStore.swift index c6fbe12..57400b8 100644 --- a/Sources/CohesionKit/EntityStore.swift +++ b/Sources/CohesionKit/EntityStore.swift @@ -180,6 +180,14 @@ public class EntityStore { return node } + for (childRef, _) in node.metadata.childrenRefs { + guard let childNode = storage[childRef]?.unwrap() as? AnyEntityNode else { + continue + } + + childNode.removeParent(node) + } + // clear all children to avoid a removed child to be kept as child node.removeAllChildren() @@ -197,6 +205,14 @@ public class EntityStore { logger?.didFailedToStore(T.self, id: entity.id, error: error) } + for parentRef in node.metadata.parentsRefs { + guard let parentNode = storage[parentRef]?.unwrap() as? AnyEntityNode else { + continue + } + + parentNode.updateEntityRelationship(node) + } + return node } diff --git a/Sources/CohesionKit/Storage/EntitiesStorage.swift b/Sources/CohesionKit/Storage/EntitiesStorage.swift index 6679ac5..935ac24 100644 --- a/Sources/CohesionKit/Storage/EntitiesStorage.swift +++ b/Sources/CohesionKit/Storage/EntitiesStorage.swift @@ -19,6 +19,11 @@ struct EntitiesStorage { set { indexes[key(for: T.self, id: 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)" } diff --git a/Sources/CohesionKit/Storage/EntityNode.swift b/Sources/CohesionKit/Storage/EntityNode.swift index 4547f76..9718fb0 100644 --- a/Sources/CohesionKit/Storage/EntityNode.swift +++ b/Sources/CohesionKit/Storage/EntityNode.swift @@ -3,12 +3,12 @@ import Combine struct EntityMetadata { /// children this entity is referencing/using - /// key: the children keypath in the parent, value: the key in EntitieStorage - // TODO: change value to a ObjectKey - var childrenRefs: [AnyKeyPath: String] = [:] + // TODO: change key to a ObjectKey + var childrenRefs: [String: AnyKeyPath] = [:] /// parents referencing this entity. This means this entity should be listed inside its parents `EntityMetadata.childrenRefs` attribute - var parentsRefs: Set = [] + // TODO: Change value to ObjectKey + var parentsRefs: Set = [] /// alias referencing this entity var aliasesRefs: Set = [] @@ -24,8 +24,11 @@ struct EntityMetadata { protocol AnyEntityNode: AnyObject { var value: Any { get } var metadata: EntityMetadata { get } + var storageKey: String { get } func nullify() + func removeParent(_ node: AnyEntityNode) + func updateEntityRelationship(_ node: EntityNode) } /// A graph node representing a entity of type `T` and its children. Anytime one of its children is updated the node @@ -93,10 +96,30 @@ class EntityNode: AnyEntityNode { func removeAllChildren() { children = [:] + metadata.childrenRefs = [:] + } + + func removeParent(_ node: AnyEntityNode) { + metadata.parentsRefs.remove(node.storageKey) + } + + func updateEntityRelationship(_ node: EntityNode) { + guard let keyPath = metadata.childrenRefs[node.storageKey] else { + return + } + + guard let writableKeyPath = keyPath as? WritableKeyPath else { + return + } + + ref.value[keyPath: writableKeyPath] = node.ref.value } /// observe one of the node child func observeChild(_ childNode: EntityNode, for keyPath: WritableKeyPath) { + metadata.childrenRefs[childNode.storageKey] = keyPath + childNode.metadata.parentsRefs.insert(storageKey) + observeChild(childNode, identity: keyPath) { root, newValue in root[keyPath: keyPath] = newValue } diff --git a/Tests/CohesionKitTests/Storage/EntityNodeTests.swift b/Tests/CohesionKitTests/Storage/EntityNodeTests.swift index 9635061..bf70e8f 100644 --- a/Tests/CohesionKitTests/Storage/EntityNodeTests.swift +++ b/Tests/CohesionKitTests/Storage/EntityNodeTests.swift @@ -89,7 +89,7 @@ class EntityNodeTests: XCTestCase { observerCalled = true } - node = EntityNode(ref: entityRef, modifiedAt: startTimestamp) + node = EntityNode(ref: entityRef, key: "RootFixture-1", modifiedAt: startTimestamp) node.observeChild(childNode, for: \.singleNode) try childNode.updateEntity(newChild, modifiedAt: nil) From 6a4d65f6336f79a9990c5753b406843f2a52a96e Mon Sep 17 00:00:00 2001 From: pjechris Date: Tue, 27 Aug 2024 09:42:04 +0200 Subject: [PATCH 4/9] enqueue changes from EntityStore --- Sources/CohesionKit/EntityStore.swift | 7 ++++++- Sources/CohesionKit/Storage/EntityNode.swift | 21 ++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Sources/CohesionKit/EntityStore.swift b/Sources/CohesionKit/EntityStore.swift index 57400b8..03982cd 100644 --- a/Sources/CohesionKit/EntityStore.swift +++ b/Sources/CohesionKit/EntityStore.swift @@ -162,6 +162,7 @@ public class EntityStore { do { try node.updateEntity(entity, modifiedAt: modifiedAt) + registry.enqueueChange(for: node) logger?.didStore(T.self, id: entity.id) } catch { @@ -199,6 +200,7 @@ public class EntityStore { do { try node.updateEntity(entity, modifiedAt: modifiedAt) + registry.enqueueChange(for: node) logger?.didStore(T.self, id: entity.id) } catch { @@ -211,6 +213,7 @@ public class EntityStore { } parentNode.updateEntityRelationship(node) + parentNode.enqueue(in: registry) } return node @@ -402,7 +405,9 @@ extension EntityStore { private func removeAliases() { for (_, node) in refAliases { - node.nullify() + if node.nullify() { + node.enqueue(in: registry) + } } } } diff --git a/Sources/CohesionKit/Storage/EntityNode.swift b/Sources/CohesionKit/Storage/EntityNode.swift index 9718fb0..40ac867 100644 --- a/Sources/CohesionKit/Storage/EntityNode.swift +++ b/Sources/CohesionKit/Storage/EntityNode.swift @@ -26,9 +26,10 @@ protocol AnyEntityNode: AnyObject { var metadata: EntityMetadata { get } var storageKey: String { get } - func nullify() + func nullify() -> Bool func removeParent(_ node: AnyEntityNode) func updateEntityRelationship(_ node: EntityNode) + func enqueue(in: ObserverRegistry) } /// A graph node representing a entity of type `T` and its children. Anytime one of its children is updated the node @@ -85,13 +86,21 @@ class EntityNode: AnyEntityNode { modifiedAt = newModifiedAt ?? modifiedAt ref.value = newEntity - onChange?(self) +// onChange?(self) } - func nullify() { + func nullify() -> Bool { if let value = ref.value as? Nullable { - try? updateEntity(value.nullified() as! T, modifiedAt: nil) + do { + try updateEntity(value.nullified() as! T, modifiedAt: nil) + return true + } + catch { + return false + } } + + return false } func removeAllChildren() { @@ -115,6 +124,10 @@ class EntityNode: AnyEntityNode { ref.value[keyPath: writableKeyPath] = node.ref.value } + func enqueue(in registry: ObserverRegistry) { + registry.enqueueChange(for: self) + } + /// observe one of the node child func observeChild(_ childNode: EntityNode, for keyPath: WritableKeyPath) { metadata.childrenRefs[childNode.storageKey] = keyPath From cffc43b2dbcb346df3b6511a5eca127d54dbbaf0 Mon Sep 17 00:00:00 2001 From: pjechris Date: Tue, 27 Aug 2024 11:17:17 +0200 Subject: [PATCH 5/9] update tests --- Sources/CohesionKit/EntityStore.swift | 9 ++++++ Sources/CohesionKit/Storage/EntityNode.swift | 6 ++-- .../Storage/EntityNodeTests.swift | 30 +++++++++---------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/Sources/CohesionKit/EntityStore.swift b/Sources/CohesionKit/EntityStore.swift index 03982cd..707000a 100644 --- a/Sources/CohesionKit/EntityStore.swift +++ b/Sources/CohesionKit/EntityStore.swift @@ -169,6 +169,15 @@ public class EntityStore { logger?.didFailedToStore(T.self, id: entity.id, error: error) } + for parentRef in node.metadata.parentsRefs { + guard let parentNode = storage[parentRef]?.unwrap() as? AnyEntityNode else { + continue + } + + parentNode.updateEntityRelationship(node) + parentNode.enqueue(in: registry) + } + return node } diff --git a/Sources/CohesionKit/Storage/EntityNode.swift b/Sources/CohesionKit/Storage/EntityNode.swift index 40ac867..4303c49 100644 --- a/Sources/CohesionKit/Storage/EntityNode.swift +++ b/Sources/CohesionKit/Storage/EntityNode.swift @@ -130,9 +130,6 @@ class EntityNode: AnyEntityNode { /// observe one of the node child func observeChild(_ childNode: EntityNode, for keyPath: WritableKeyPath) { - metadata.childrenRefs[childNode.storageKey] = keyPath - childNode.metadata.parentsRefs.insert(storageKey) - observeChild(childNode, identity: keyPath) { root, newValue in root[keyPath: keyPath] = newValue } @@ -154,6 +151,9 @@ class EntityNode: AnyEntityNode { identity keyPath: KeyPath, update: @escaping (inout T, Element) -> Void ) { + metadata.childrenRefs[childNode.storageKey] = keyPath + childNode.metadata.parentsRefs.insert(storageKey) + if let subscribedChild = children[keyPath]?.node as? EntityNode, subscribedChild == childNode { return } diff --git a/Tests/CohesionKitTests/Storage/EntityNodeTests.swift b/Tests/CohesionKitTests/Storage/EntityNodeTests.swift index bf70e8f..20849ed 100644 --- a/Tests/CohesionKitTests/Storage/EntityNodeTests.swift +++ b/Tests/CohesionKitTests/Storage/EntityNodeTests.swift @@ -68,35 +68,33 @@ class EntityNodeTests: XCTestCase { } } - func test_observeChild_childChange_entityIsUpdated() throws { + func test_observeChild_nodeIsAddedAsParentMetadata() { let childNode = EntityNode(startEntity.singleNode, modifiedAt: nil) - let newChild = SingleNodeFixture(id: 1, primitive: "updated") node.observeChild(childNode, for: \.singleNode) - try childNode.updateEntity(newChild, modifiedAt: nil) + XCTAssertTrue(childNode.metadata.parentsRefs.contains(node.storageKey)) + } + + func test_observeChild_childrenMetadataIsUpdated() { + let childNode = EntityNode(startEntity.singleNode, modifiedAt: nil) - XCTAssertEqual((node.value as? RootFixture)?.singleNode, newChild) + node.observeChild(childNode, for: \.singleNode) + + XCTAssertTrue(node.metadata.childrenRefs.keys.contains(childNode.storageKey)) } - func test_observeChild_childChange_entityObserversAreCalled() throws { + func test_updateEntityRelationship_childIsUpdated() throws { let childNode = EntityNode(startEntity.singleNode, modifiedAt: startTimestamp) let newChild = SingleNodeFixture(id: 1, primitive: "updated") - let entityRef = Observable(value: startEntity) - var observerCalled = false - - let subscription = entityRef.addObserver { _ in - observerCalled = true - } - node = EntityNode(ref: entityRef, key: "RootFixture-1", modifiedAt: startTimestamp) node.observeChild(childNode, for: \.singleNode) try childNode.updateEntity(newChild, modifiedAt: nil) - subscription.unsubscribe() + node.updateEntityRelationship(childNode) - XCTAssertTrue(observerCalled) + XCTAssertEqual(node.ref.value.singleNode, newChild) } func test_observeChild_childIsCollection_eachChildIsAdded() { @@ -104,11 +102,11 @@ class EntityNodeTests: XCTestCase { let child2 = EntityNode(ListNodeFixture(id: 2), modifiedAt: startTimestamp) let node = EntityNode(startEntity, modifiedAt: startTimestamp) - XCTAssertEqual(node.children.count, 0) + XCTAssertEqual(node.metadata.childrenRefs.count, 0) node.observeChild(child1, for: \.listNodes[0]) node.observeChild(child2, for: \.listNodes[1]) - XCTAssertEqual(node.children.count, 2) + XCTAssertEqual(node.metadata.childrenRefs.count, 2) } } From be7ed2c6fd2cd111c7bac396b1d4ff93a068c294 Mon Sep 17 00:00:00 2001 From: pjechris Date: Tue, 27 Aug 2024 11:36:44 +0200 Subject: [PATCH 6/9] don't use observable anymore --- Sources/CohesionKit/Storage/EntityNode.swift | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/Sources/CohesionKit/Storage/EntityNode.swift b/Sources/CohesionKit/Storage/EntityNode.swift index 4303c49..4ec937d 100644 --- a/Sources/CohesionKit/Storage/EntityNode.swift +++ b/Sources/CohesionKit/Storage/EntityNode.swift @@ -46,6 +46,8 @@ class EntityNode: AnyEntityNode { var value: Any { ref.value } var metadata = EntityMetadata() + // FIXME: to delete, it's "just" to have a strong ref and avoid nodes to be deleted. Need a better memory management + private var childrenNodes: [AnyEntityNode] = [] var applyChildrenChanges = true /// An observable entity reference @@ -86,7 +88,6 @@ class EntityNode: AnyEntityNode { modifiedAt = newModifiedAt ?? modifiedAt ref.value = newEntity -// onChange?(self) } func nullify() -> Bool { @@ -106,6 +107,7 @@ class EntityNode: AnyEntityNode { func removeAllChildren() { children = [:] metadata.childrenRefs = [:] + childrenNodes = [] } func removeParent(_ node: AnyEntityNode) { @@ -153,21 +155,7 @@ class EntityNode: AnyEntityNode { ) { metadata.childrenRefs[childNode.storageKey] = keyPath childNode.metadata.parentsRefs.insert(storageKey) - - if let subscribedChild = children[keyPath]?.node as? EntityNode, subscribedChild == childNode { - return - } - - let subscription = childNode.ref.addObserver { [unowned self] newValue in - guard self.applyChildrenChanges else { - return - } - - update(&self.ref.value, newValue) - self.onChange?(self) - } - - children[keyPath] = SubscribedChild(subscription: subscription, node: childNode) + childrenNodes.append(childNode) } } From 57f533b2b237e7e0915f6dddab771e8764ae84db Mon Sep 17 00:00:00 2001 From: pjechris Date: Wed, 28 Aug 2024 21:11:35 +0200 Subject: [PATCH 7/9] update parents of parents! --- Sources/CohesionKit/EntityStore.swift | 22 +++++++--------- .../CohesionKit/Storage/AliasStorage.swift | 5 ++-- Sources/CohesionKit/Storage/EntityNode.swift | 26 +++++++++++++------ Tests/CohesionKitTests/EntityStoreTests.swift | 2 +- .../Visitor/EntityStoreVisitorTests.swift | 4 +-- 5 files changed, 34 insertions(+), 25 deletions(-) diff --git a/Sources/CohesionKit/EntityStore.swift b/Sources/CohesionKit/EntityStore.swift index 707000a..dd78b36 100644 --- a/Sources/CohesionKit/EntityStore.swift +++ b/Sources/CohesionKit/EntityStore.swift @@ -169,14 +169,7 @@ public class EntityStore { logger?.didFailedToStore(T.self, id: entity.id, error: error) } - for parentRef in node.metadata.parentsRefs { - guard let parentNode = storage[parentRef]?.unwrap() as? AnyEntityNode else { - continue - } - - parentNode.updateEntityRelationship(node) - parentNode.enqueue(in: registry) - } + updateParents(of: node) return node } @@ -191,7 +184,7 @@ public class EntityStore { } for (childRef, _) in node.metadata.childrenRefs { - guard let childNode = storage[childRef]?.unwrap() as? AnyEntityNode else { + guard let childNode = storage[childRef]?.unwrap() as? any AnyEntityNode else { continue } @@ -216,16 +209,21 @@ public class EntityStore { logger?.didFailedToStore(T.self, id: entity.id, error: error) } + updateParents(of: node) + + return node + } + + func updateParents(of node: some AnyEntityNode) { for parentRef in node.metadata.parentsRefs { - guard let parentNode = storage[parentRef]?.unwrap() as? AnyEntityNode else { + guard let parentNode = storage[parentRef]?.unwrap() as? any AnyEntityNode ?? refAliases[parentRef] else { continue } parentNode.updateEntityRelationship(node) parentNode.enqueue(in: registry) + updateParents(of: parentNode) } - - return node } private func storeAlias(content: T?, key: AliasKey, modifiedAt: Stamp?) { diff --git a/Sources/CohesionKit/Storage/AliasStorage.swift b/Sources/CohesionKit/Storage/AliasStorage.swift index a8a341e..f46cf9a 100644 --- a/Sources/CohesionKit/Storage/AliasStorage.swift +++ b/Sources/CohesionKit/Storage/AliasStorage.swift @@ -1,5 +1,5 @@ /// Keep a strong reference on each aliased node -typealias AliasStorage = [String: AnyEntityNode] +typealias AliasStorage = [String: any AnyEntityNode] extension AliasStorage { subscript(_ aliasKey: AliasKey) -> EntityNode>? { @@ -9,7 +9,8 @@ extension AliasStorage { subscript(safe key: AliasKey, onChange onChange: ((EntityNode>) -> Void)? = nil) -> EntityNode> { mutating get { - self[key: key, default: EntityNode(AliasContainer(key: key), modifiedAt: nil, onChange: onChange)] + let storeKey = buildKey(for: T.self, key: key) + return self[key: key, default: EntityNode(AliasContainer(key: key), key: storeKey, modifiedAt: nil, onChange: onChange)] } } diff --git a/Sources/CohesionKit/Storage/EntityNode.swift b/Sources/CohesionKit/Storage/EntityNode.swift index 4ec937d..88cb35b 100644 --- a/Sources/CohesionKit/Storage/EntityNode.swift +++ b/Sources/CohesionKit/Storage/EntityNode.swift @@ -22,32 +22,36 @@ struct EntityMetadata { /// Typed erased protocol protocol AnyEntityNode: AnyObject { + associatedtype Value + + var ref: Observable { get } var value: Any { get } var metadata: EntityMetadata { get } var storageKey: String { get } func nullify() -> Bool - func removeParent(_ node: AnyEntityNode) - func updateEntityRelationship(_ node: EntityNode) + func removeParent(_ node: any AnyEntityNode) + func updateEntityRelationship(_ node: some AnyEntityNode) func enqueue(in: ObserverRegistry) } /// A graph node representing a entity of type `T` and its children. Anytime one of its children is updated the node /// will reflect the change on its own value. class EntityNode: AnyEntityNode { + typealias Value = T /// A child subscription used by its EntityNode parent struct SubscribedChild { /// the child subscription. Use it to unsubscribe to child upates let subscription: Subscription /// the child node value - let node: AnyEntityNode + let node: any AnyEntityNode } var value: Any { ref.value } var metadata = EntityMetadata() // FIXME: to delete, it's "just" to have a strong ref and avoid nodes to be deleted. Need a better memory management - private var childrenNodes: [AnyEntityNode] = [] + private var childrenNodes: [any AnyEntityNode] = [] var applyChildrenChanges = true /// An observable entity reference @@ -110,20 +114,26 @@ class EntityNode: AnyEntityNode { childrenNodes = [] } - func removeParent(_ node: AnyEntityNode) { + func removeParent(_ node: any AnyEntityNode) { metadata.parentsRefs.remove(node.storageKey) } - func updateEntityRelationship(_ node: EntityNode) { + func updateEntityRelationship(_ node: U) { guard let keyPath = metadata.childrenRefs[node.storageKey] else { return } - guard let writableKeyPath = keyPath as? WritableKeyPath else { + if let writableKeyPath = keyPath as? WritableKeyPath { + ref.value[keyPath: writableKeyPath] = node.ref.value + return + } + + if let optionalWritableKeyPath = keyPath as? WritableKeyPath { + ref.value[keyPath: optionalWritableKeyPath] = node.ref.value return } - ref.value[keyPath: writableKeyPath] = node.ref.value + print("CohesionKit: cannot convert \(type(of: keyPath)) to WritableKeyPath<\(T.self), \(U.Value.self)>") } func enqueue(in registry: ObserverRegistry) { diff --git a/Tests/CohesionKitTests/EntityStoreTests.swift b/Tests/CohesionKitTests/EntityStoreTests.swift index d2c0661..381908b 100644 --- a/Tests/CohesionKitTests/EntityStoreTests.swift +++ b/Tests/CohesionKitTests/EntityStoreTests.swift @@ -411,7 +411,7 @@ extension EntityStoreTests { XCTAssertTrue(registry.hasPendingChange(for: AliasContainer.self)) } - func test_update_entityIsIndirectlyUsedByAlias_itEnqueuesAliasInRegistry() { + func test_update_entityIsInsideAggregagte_aggreateIsAliased_itEnqueuesAliasInRegistry() { let aggregate = RootFixture(id: 1, primitive: "", singleNode: SingleNodeFixture(id: 1), listNodes: []) let registry = ObserverRegistryStub() let entityStore = EntityStore(registry: registry) diff --git a/Tests/CohesionKitTests/Visitor/EntityStoreVisitorTests.swift b/Tests/CohesionKitTests/Visitor/EntityStoreVisitorTests.swift index e2e9984..e0ea9e1 100644 --- a/Tests/CohesionKitTests/Visitor/EntityStoreVisitorTests.swift +++ b/Tests/CohesionKitTests/Visitor/EntityStoreVisitorTests.swift @@ -59,8 +59,8 @@ class EntityStoreStoreVisitorTests: XCTestCase { } private class EntityNodeStub: EntityNode { - var observeChildKeyPathCalled: (AnyEntityNode, PartialKeyPath) -> Void = { _, _ in } - var observeChildKeyPathOptionalCalled: (AnyEntityNode, PartialKeyPath) -> Void = { _, _ in } + var observeChildKeyPathCalled: (any AnyEntityNode, PartialKeyPath) -> Void = { _, _ in } + var observeChildKeyPathOptionalCalled: (any AnyEntityNode, PartialKeyPath) -> Void = { _, _ in } override func observeChild(_ childNode: EntityNode, for keyPath: WritableKeyPath) { observeChildKeyPathCalled(childNode, keyPath) From 43fcc4003e16a5de9d292b523df8c26bb2e488dc Mon Sep 17 00:00:00 2001 From: pjechris Date: Thu, 29 Aug 2024 13:57:56 +0200 Subject: [PATCH 8/9] add test about updating parents --- Tests/CohesionKitTests/EntityStoreTests.swift | 16 +++++++++++++++ Tests/CohesionKitTests/RootFixture.swift | 20 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Tests/CohesionKitTests/EntityStoreTests.swift b/Tests/CohesionKitTests/EntityStoreTests.swift index 381908b..fffbe58 100644 --- a/Tests/CohesionKitTests/EntityStoreTests.swift +++ b/Tests/CohesionKitTests/EntityStoreTests.swift @@ -411,6 +411,22 @@ extension EntityStoreTests { XCTAssertTrue(registry.hasPendingChange(for: AliasContainer.self)) } + // make sure that when we have A -> B -> C and update C, we enqueue parents B AND A. + func test_update_entityIsNested_itEnqueuesAllParents() { + let a = AFixture(b: BFixture(c: SingleNodeFixture(id: 1))) + let registry = ObserverRegistryStub() + let entityStore = EntityStore(registry: registry) + + withExtendedLifetime(entityStore.store(entity: a)) { + registry.clearPendingChangesStub() + + _ = entityStore.nodeStore(entity: SingleNodeFixture(id: 1, primitive: "updated"), modifiedAt: nil) + } + + XCTAssertTrue(registry.hasPendingChange(for: BFixture.self)) + XCTAssertTrue(registry.hasPendingChange(for: AFixture.self)) + } + func test_update_entityIsInsideAggregagte_aggreateIsAliased_itEnqueuesAliasInRegistry() { let aggregate = RootFixture(id: 1, primitive: "", singleNode: SingleNodeFixture(id: 1), listNodes: []) let registry = ObserverRegistryStub() diff --git a/Tests/CohesionKitTests/RootFixture.swift b/Tests/CohesionKitTests/RootFixture.swift index 7f353aa..e81a614 100644 --- a/Tests/CohesionKitTests/RootFixture.swift +++ b/Tests/CohesionKitTests/RootFixture.swift @@ -1,6 +1,24 @@ import Foundation import CohesionKit +struct AFixture: Aggregate { + var id: BFixture.ID { b.id } + var b: BFixture + + var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath] { + [.init(\.b)] + } +} + +struct BFixture: Aggregate { + var id: SingleNodeFixture.ID { c.id } + var c: SingleNodeFixture + + var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath] { + [.init(\.c)] + } +} + struct RootFixture: Aggregate, Equatable { let id: Int let primitive: String @@ -56,4 +74,4 @@ struct ListNodeFixture: Identifiable, Equatable { PartialIdentifiableKeyPath(parent.appending(path: \.singleNode)) ] } -} \ No newline at end of file +} From aa5c1198c784c3f71892b472adc25ddf408d4c87 Mon Sep 17 00:00:00 2001 From: pjechris Date: Thu, 29 Aug 2024 14:01:49 +0200 Subject: [PATCH 9/9] keep applyChildrenChanges --- Sources/CohesionKit/Storage/EntityNode.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Sources/CohesionKit/Storage/EntityNode.swift b/Sources/CohesionKit/Storage/EntityNode.swift index 88cb35b..d11d391 100644 --- a/Sources/CohesionKit/Storage/EntityNode.swift +++ b/Sources/CohesionKit/Storage/EntityNode.swift @@ -31,7 +31,7 @@ protocol AnyEntityNode: AnyObject { func nullify() -> Bool func removeParent(_ node: any AnyEntityNode) - func updateEntityRelationship(_ node: some AnyEntityNode) + func updateEntityRelationship(_ child: some AnyEntityNode) func enqueue(in: ObserverRegistry) } @@ -118,18 +118,22 @@ class EntityNode: AnyEntityNode { metadata.parentsRefs.remove(node.storageKey) } - func updateEntityRelationship(_ node: U) { - guard let keyPath = metadata.childrenRefs[node.storageKey] else { + func updateEntityRelationship(_ child: U) { + guard applyChildrenChanges else { + return + } + + guard let keyPath = metadata.childrenRefs[child.storageKey] else { return } if let writableKeyPath = keyPath as? WritableKeyPath { - ref.value[keyPath: writableKeyPath] = node.ref.value + ref.value[keyPath: writableKeyPath] = child.ref.value return } if let optionalWritableKeyPath = keyPath as? WritableKeyPath { - ref.value[keyPath: optionalWritableKeyPath] = node.ref.value + ref.value[keyPath: optionalWritableKeyPath] = child.ref.value return }