Skip to content

Commit 2c0ca28

Browse files
author
pjechris
authored
tech(entity): Introduce metadata system (#72)
1 parent 13f43e6 commit 2c0ca28

File tree

8 files changed

+179
-45
lines changed

8 files changed

+179
-45
lines changed

Sources/CohesionKit/EntityStore.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,15 @@ public class EntityStore {
162162

163163
do {
164164
try node.updateEntity(entity, modifiedAt: modifiedAt)
165+
registry.enqueueChange(for: node)
165166
logger?.didStore(T.self, id: entity.id)
166167
}
167168
catch {
168169
logger?.didFailedToStore(T.self, id: entity.id, error: error)
169170
}
170171

172+
updateParents(of: node)
173+
171174
return node
172175
}
173176

@@ -180,6 +183,14 @@ public class EntityStore {
180183
return node
181184
}
182185

186+
for (childRef, _) in node.metadata.childrenRefs {
187+
guard let childNode = storage[childRef]?.unwrap() as? any AnyEntityNode else {
188+
continue
189+
}
190+
191+
childNode.removeParent(node)
192+
}
193+
183194
// clear all children to avoid a removed child to be kept as child
184195
node.removeAllChildren()
185196

@@ -191,15 +202,30 @@ public class EntityStore {
191202

192203
do {
193204
try node.updateEntity(entity, modifiedAt: modifiedAt)
205+
registry.enqueueChange(for: node)
194206
logger?.didStore(T.self, id: entity.id)
195207
}
196208
catch {
197209
logger?.didFailedToStore(T.self, id: entity.id, error: error)
198210
}
199211

212+
updateParents(of: node)
213+
200214
return node
201215
}
202216

217+
func updateParents(of node: some AnyEntityNode) {
218+
for parentRef in node.metadata.parentsRefs {
219+
guard let parentNode = storage[parentRef]?.unwrap() as? any AnyEntityNode ?? refAliases[parentRef] else {
220+
continue
221+
}
222+
223+
parentNode.updateEntityRelationship(node)
224+
parentNode.enqueue(in: registry)
225+
updateParents(of: parentNode)
226+
}
227+
}
228+
203229
private func storeAlias<T>(content: T?, key: AliasKey<T>, modifiedAt: Stamp?) {
204230
let aliasNode = refAliases[safe: key, onChange: registry.enqueueChange(for:)]
205231
let aliasContainer = AliasContainer(key: key, content: content)
@@ -386,7 +412,9 @@ extension EntityStore {
386412

387413
private func removeAliases() {
388414
for (_, node) in refAliases {
389-
node.nullify()
415+
if node.nullify() {
416+
node.enqueue(in: registry)
417+
}
390418
}
391419
}
392420
}

Sources/CohesionKit/Storage/AliasStorage.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/// Keep a strong reference on each aliased node
2-
typealias AliasStorage = [String: AnyEntityNode]
2+
typealias AliasStorage = [String: any AnyEntityNode]
33

44
extension AliasStorage {
55
subscript<T>(_ aliasKey: AliasKey<T>) -> EntityNode<AliasContainer<T>>? {
@@ -9,7 +9,8 @@ extension AliasStorage {
99

1010
subscript<T>(safe key: AliasKey<T>, onChange onChange: ((EntityNode<AliasContainer<T>>) -> Void)? = nil) -> EntityNode<AliasContainer<T>> {
1111
mutating get {
12-
self[key: key, default: EntityNode(AliasContainer(key: key), modifiedAt: nil, onChange: onChange)]
12+
let storeKey = buildKey(for: T.self, key: key)
13+
return self[key: key, default: EntityNode(AliasContainer(key: key), key: storeKey, modifiedAt: nil, onChange: onChange)]
1314
}
1415
}
1516

Sources/CohesionKit/Storage/EntitiesStorage.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ struct EntitiesStorage {
1919
set { indexes[key(for: T.self, id: id)] = Weak(value: newValue) }
2020
}
2121

22+
subscript(_ key: String) -> AnyWeak? {
23+
get { indexes[key] }
24+
set { indexes[key] = newValue }
25+
}
26+
2227
private func key<T>(for type: T.Type, id: Any) -> String {
2328
"\(type)-\(id)"
2429
}

Sources/CohesionKit/Storage/EntityNode.swift

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,85 @@
11
import Foundation
22
import Combine
33

4+
struct EntityMetadata {
5+
/// children this entity is referencing/using
6+
// TODO: change key to a ObjectKey
7+
var childrenRefs: [String: AnyKeyPath] = [:]
8+
9+
/// parents referencing this entity. This means this entity should be listed inside its parents `EntityMetadata.childrenRefs` attribute
10+
// TODO: Change value to ObjectKey
11+
var parentsRefs: Set<String> = []
12+
/// alias referencing this entity
13+
var aliasesRefs: Set<String> = []
14+
15+
/// number of observers
16+
var observersCount: Int = 0
17+
18+
var isActivelyUsed: Bool {
19+
observersCount > 0 || !parentsRefs.isEmpty || !aliasesRefs.isEmpty
20+
}
21+
}
22+
423
/// Typed erased protocol
524
protocol AnyEntityNode: AnyObject {
25+
associatedtype Value
26+
27+
var ref: Observable<Value> { get }
628
var value: Any { get }
29+
var metadata: EntityMetadata { get }
30+
var storageKey: String { get }
731

8-
func nullify()
32+
func nullify() -> Bool
33+
func removeParent(_ node: any AnyEntityNode)
34+
func updateEntityRelationship(_ child: some AnyEntityNode)
35+
func enqueue(in: ObserverRegistry)
936
}
1037

1138
/// A graph node representing a entity of type `T` and its children. Anytime one of its children is updated the node
1239
/// will reflect the change on its own value.
1340
class EntityNode<T>: AnyEntityNode {
41+
typealias Value = T
1442
/// A child subscription used by its EntityNode parent
1543
struct SubscribedChild {
1644
/// the child subscription. Use it to unsubscribe to child upates
1745
let subscription: Subscription
1846
/// the child node value
19-
let node: AnyEntityNode
47+
let node: any AnyEntityNode
2048
}
2149

2250
var value: Any { ref.value }
2351

52+
var metadata = EntityMetadata()
53+
// FIXME: to delete, it's "just" to have a strong ref and avoid nodes to be deleted. Need a better memory management
54+
private var childrenNodes: [any AnyEntityNode] = []
55+
2456
var applyChildrenChanges = true
2557
/// An observable entity reference
2658
let ref: Observable<T>
2759

60+
let storageKey: String
61+
2862
private let onChange: ((EntityNode<T>) -> Void)?
2963
/// last time the ref.value was changed. Any subsequent change must have a higher value to be applied
3064
/// if nil ref has no stamp and any change will be accepted
3165
private var modifiedAt: Stamp?
3266
/// entity children
3367
private(set) var children: [PartialKeyPath<T>: SubscribedChild] = [:]
3468

35-
init(ref: Observable<T>, modifiedAt: Stamp?, onChange: ((EntityNode<T>) -> Void)? = nil) {
69+
init(ref: Observable<T>, key: String, modifiedAt: Stamp?, onChange: ((EntityNode<T>) -> Void)? = nil) {
3670
self.ref = ref
3771
self.modifiedAt = modifiedAt
3872
self.onChange = onChange
73+
self.storageKey = key
74+
}
75+
76+
convenience init(_ entity: T, key: String, modifiedAt: Stamp?, onChange: ((EntityNode<T>) -> Void)? = nil) {
77+
self.init(ref: Observable(value: entity), key: key, modifiedAt: modifiedAt, onChange: onChange)
3978
}
4079

41-
convenience init(_ entity: T, modifiedAt: Stamp?, onChange: ((EntityNode<T>) -> Void)? = nil) {
42-
self.init(ref: Observable(value: entity), modifiedAt: modifiedAt, onChange: onChange)
80+
convenience init(_ entity: T, modifiedAt: Stamp?, onChange: ((EntityNode<T>) -> Void)? = nil) where T: Identifiable {
81+
let key = "\(T.self)-\(entity.id)"
82+
self.init(entity, key: key, modifiedAt: modifiedAt, onChange: onChange)
4383
}
4484

4585
/// change the entity to a new value. If modifiedAt is nil or > to previous date update the value will be changed
@@ -52,17 +92,56 @@ class EntityNode<T>: AnyEntityNode {
5292

5393
modifiedAt = newModifiedAt ?? modifiedAt
5494
ref.value = newEntity
55-
onChange?(self)
5695
}
5796

58-
func nullify() {
97+
func nullify() -> Bool {
5998
if let value = ref.value as? Nullable {
60-
try? updateEntity(value.nullified() as! T, modifiedAt: nil)
99+
do {
100+
try updateEntity(value.nullified() as! T, modifiedAt: nil)
101+
return true
102+
}
103+
catch {
104+
return false
105+
}
61106
}
107+
108+
return false
62109
}
63110

64111
func removeAllChildren() {
65112
children = [:]
113+
metadata.childrenRefs = [:]
114+
childrenNodes = []
115+
}
116+
117+
func removeParent(_ node: any AnyEntityNode) {
118+
metadata.parentsRefs.remove(node.storageKey)
119+
}
120+
121+
func updateEntityRelationship<U: AnyEntityNode>(_ child: U) {
122+
guard applyChildrenChanges else {
123+
return
124+
}
125+
126+
guard let keyPath = metadata.childrenRefs[child.storageKey] else {
127+
return
128+
}
129+
130+
if let writableKeyPath = keyPath as? WritableKeyPath<T, U.Value> {
131+
ref.value[keyPath: writableKeyPath] = child.ref.value
132+
return
133+
}
134+
135+
if let optionalWritableKeyPath = keyPath as? WritableKeyPath<T, U.Value?> {
136+
ref.value[keyPath: optionalWritableKeyPath] = child.ref.value
137+
return
138+
}
139+
140+
print("CohesionKit: cannot convert \(type(of: keyPath)) to WritableKeyPath<\(T.self), \(U.Value.self)>")
141+
}
142+
143+
func enqueue(in registry: ObserverRegistry) {
144+
registry.enqueueChange(for: self)
66145
}
67146

68147
/// observe one of the node child
@@ -88,20 +167,9 @@ class EntityNode<T>: AnyEntityNode {
88167
identity keyPath: KeyPath<T, C>,
89168
update: @escaping (inout T, Element) -> Void
90169
) {
91-
if let subscribedChild = children[keyPath]?.node as? EntityNode<Element>, subscribedChild == childNode {
92-
return
93-
}
94-
95-
let subscription = childNode.ref.addObserver { [unowned self] newValue in
96-
guard self.applyChildrenChanges else {
97-
return
98-
}
99-
100-
update(&self.ref.value, newValue)
101-
self.onChange?(self)
102-
}
103-
104-
children[keyPath] = SubscribedChild(subscription: subscription, node: childNode)
170+
metadata.childrenRefs[childNode.storageKey] = keyPath
171+
childNode.metadata.parentsRefs.insert(storageKey)
172+
childrenNodes.append(childNode)
105173
}
106174
}
107175

Tests/CohesionKitTests/EntityStoreTests.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,23 @@ extension EntityStoreTests {
411411
XCTAssertTrue(registry.hasPendingChange(for: AliasContainer<RootFixture>.self))
412412
}
413413

414-
func test_update_entityIsIndirectlyUsedByAlias_itEnqueuesAliasInRegistry() {
414+
// make sure that when we have A -> B -> C and update C, we enqueue parents B AND A.
415+
func test_update_entityIsNested_itEnqueuesAllParents() {
416+
let a = AFixture(b: BFixture(c: SingleNodeFixture(id: 1)))
417+
let registry = ObserverRegistryStub()
418+
let entityStore = EntityStore(registry: registry)
419+
420+
withExtendedLifetime(entityStore.store(entity: a)) {
421+
registry.clearPendingChangesStub()
422+
423+
_ = entityStore.nodeStore(entity: SingleNodeFixture(id: 1, primitive: "updated"), modifiedAt: nil)
424+
}
425+
426+
XCTAssertTrue(registry.hasPendingChange(for: BFixture.self))
427+
XCTAssertTrue(registry.hasPendingChange(for: AFixture.self))
428+
}
429+
430+
func test_update_entityIsInsideAggregagte_aggreateIsAliased_itEnqueuesAliasInRegistry() {
415431
let aggregate = RootFixture(id: 1, primitive: "", singleNode: SingleNodeFixture(id: 1), listNodes: [])
416432
let registry = ObserverRegistryStub()
417433
let entityStore = EntityStore(registry: registry)

Tests/CohesionKitTests/RootFixture.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
import Foundation
22
import CohesionKit
33

4+
struct AFixture: Aggregate {
5+
var id: BFixture.ID { b.id }
6+
var b: BFixture
7+
8+
var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath<Self>] {
9+
[.init(\.b)]
10+
}
11+
}
12+
13+
struct BFixture: Aggregate {
14+
var id: SingleNodeFixture.ID { c.id }
15+
var c: SingleNodeFixture
16+
17+
var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath<Self>] {
18+
[.init(\.c)]
19+
}
20+
}
21+
422
struct RootFixture: Aggregate, Equatable {
523
let id: Int
624
let primitive: String
@@ -56,4 +74,4 @@ struct ListNodeFixture: Identifiable, Equatable {
5674
PartialIdentifiableKeyPath(parent.appending(path: \.singleNode))
5775
]
5876
}
59-
}
77+
}

Tests/CohesionKitTests/Storage/EntityNodeTests.swift

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -68,47 +68,45 @@ class EntityNodeTests: XCTestCase {
6868
}
6969
}
7070

71-
func test_observeChild_childChange_entityIsUpdated() throws {
71+
func test_observeChild_nodeIsAddedAsParentMetadata() {
7272
let childNode = EntityNode(startEntity.singleNode, modifiedAt: nil)
73-
let newChild = SingleNodeFixture(id: 1, primitive: "updated")
7473

7574
node.observeChild(childNode, for: \.singleNode)
7675

77-
try childNode.updateEntity(newChild, modifiedAt: nil)
76+
XCTAssertTrue(childNode.metadata.parentsRefs.contains(node.storageKey))
77+
}
78+
79+
func test_observeChild_childrenMetadataIsUpdated() {
80+
let childNode = EntityNode(startEntity.singleNode, modifiedAt: nil)
7881

79-
XCTAssertEqual((node.value as? RootFixture)?.singleNode, newChild)
82+
node.observeChild(childNode, for: \.singleNode)
83+
84+
XCTAssertTrue(node.metadata.childrenRefs.keys.contains(childNode.storageKey))
8085
}
8186

82-
func test_observeChild_childChange_entityObserversAreCalled() throws {
87+
func test_updateEntityRelationship_childIsUpdated() throws {
8388
let childNode = EntityNode(startEntity.singleNode, modifiedAt: startTimestamp)
8489
let newChild = SingleNodeFixture(id: 1, primitive: "updated")
85-
let entityRef = Observable(value: startEntity)
86-
var observerCalled = false
87-
88-
let subscription = entityRef.addObserver { _ in
89-
observerCalled = true
90-
}
9190

92-
node = EntityNode(ref: entityRef, modifiedAt: startTimestamp)
9391
node.observeChild(childNode, for: \.singleNode)
9492

9593
try childNode.updateEntity(newChild, modifiedAt: nil)
9694

97-
subscription.unsubscribe()
95+
node.updateEntityRelationship(childNode)
9896

99-
XCTAssertTrue(observerCalled)
97+
XCTAssertEqual(node.ref.value.singleNode, newChild)
10098
}
10199

102100
func test_observeChild_childIsCollection_eachChildIsAdded() {
103101
let child1 = EntityNode(ListNodeFixture(id: 1), modifiedAt: startTimestamp)
104102
let child2 = EntityNode(ListNodeFixture(id: 2), modifiedAt: startTimestamp)
105103
let node = EntityNode(startEntity, modifiedAt: startTimestamp)
106104

107-
XCTAssertEqual(node.children.count, 0)
105+
XCTAssertEqual(node.metadata.childrenRefs.count, 0)
108106

109107
node.observeChild(child1, for: \.listNodes[0])
110108
node.observeChild(child2, for: \.listNodes[1])
111109

112-
XCTAssertEqual(node.children.count, 2)
110+
XCTAssertEqual(node.metadata.childrenRefs.count, 2)
113111
}
114112
}

0 commit comments

Comments
 (0)