Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Sources/CohesionKit/Identifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/// 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(value)
}

/// Generates an identifier for type T with key as key
init<T>(for type: T.Type, key: Any) {
self.init("\(T.self):\(key)")
}

init<T: Identifiable>(for object: T) {
self.init(for: T.self, key: object.id)
}

init<T>(for type: T.Type, key: AliasKey<T>) {
self.init("alias:\(T.self):\(key.name)")
}
}
45 changes: 15 additions & 30 deletions Sources/CohesionKit/Observer/ObserverRegistry.swift
Original file line number Diff line number Diff line change
@@ -1,41 +1,26 @@
import Foundation

/// a unique hash identifying an object
typealias ObjectKey = Int

extension ObjectKey {
init<T>(of type: T.Type, id: Any) {
let key = "\(type)-\(id)"

self.init(key.hashValue)
}

init<T>(_ node: EntityNode<T>) {
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<Handler>] = [:]
private var handlers: [Identifier: Set<Handler>] = [:]
/// 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<T>(node: EntityNode<T>, initial: Bool = false, onChange: @escaping (T) -> Void) -> Subscription {
addObserver(node: node, key: ObjectKey(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
/// to this node `onChange` will be called.
func addObserver<T>(node: EntityNode<T>, key: ObjectKey, initial: Bool = false, onChange: @escaping (T) -> Void) -> Subscription {
func addObserver<T>(node: EntityNode<T>, identifier: Identifier, initial: Bool = false, onChange: @escaping (T) -> Void) -> Subscription {
let handler = Handler { onChange($0.value) }

if initial {
Expand All @@ -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.
Expand All @@ -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: node.id) }

return Subscription {
subscriptions.forEach { $0.unsubscribe() }
Expand All @@ -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<T>(for node: EntityNode<T>) {
enqueueChange(for: node, key: ObjectKey(node))
enqueueChange(for: node, identifier: node.id)
}

func enqueueChange<T>(for node: EntityNode<T>, key: ObjectKey) {
pendingChanges[key] = Weak(value: node)
func enqueueChange<T>(for node: EntityNode<T>, identifier: Identifier) {
pendingChanges[identifier] = Weak(value: node)
}

func hasPendingChange<T>(for node: EntityNode<T>) -> Bool {
hasPendingChange(for: ObjectKey(node))
hasPendingChange(for: node.id)
}

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.
Expand Down Expand Up @@ -123,14 +108,14 @@ class ObserverRegistry {
}
}

private func subscribeHandler<T>(_ handler: Handler, for node: EntityNode<T>, key: ObjectKey) -> Subscription {
handlers[key, default: []].insert(handler)
private func subscribeHandler<T>(_ handler: Handler, for node: EntityNode<T>, 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)
}
}
}
Expand Down
18 changes: 7 additions & 11 deletions Sources/CohesionKit/Storage/AliasStorage.swift
Original file line number Diff line number Diff line change
@@ -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<T>(_ aliasKey: AliasKey<T>) -> EntityNode<AliasContainer<T>>? {
get { self[buildKey(for: T.self, key: aliasKey)] as? EntityNode<AliasContainer<T>> }
set { self[buildKey(for: T.self, key: aliasKey)] = newValue }
get { self[Identifier(for: T.self, key: aliasKey)] as? EntityNode<AliasContainer<T>> }
set { self[Identifier(for: T.self, key: aliasKey)] = newValue }
}

subscript<T>(safe key: AliasKey<T>) -> EntityNode<AliasContainer<T>> {
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<T>(key key: AliasKey<T>, default defaultValue: @autoclosure () -> EntityNode<AliasContainer<T>>)
Expand All @@ -28,8 +28,4 @@ extension AliasStorage {
return node
}
}

private func buildKey<T>(for type: T.Type, key: AliasKey<T>) -> String {
"\(type):\(key.name)"
}
}
18 changes: 7 additions & 11 deletions Sources/CohesionKit/Storage/EntitiesStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<EntityNode<Object>>]
private typealias Storage = [String: AnyWeak]
/// the storage indexer. Stored content is [Identifier: Weak<EntityNode<Object>>]
private typealias Storage = [Identifier: AnyWeak]

private var indexes: Storage = [:]

Expand All @@ -15,17 +15,13 @@ struct EntitiesStorage {
}

subscript<T: Identifiable>(_ type: T.Type, id id: T.ID) -> EntityNode<T>? {
get { (indexes[key(for: T.self, id: id)] as? Weak<EntityNode<T>>)?.value }
set { indexes[key(for: T.self, id: id)] = Weak(value: newValue) }
get { (indexes[Identifier(for: T.self, key: id)] as? Weak<EntityNode<T>>)?.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<T>(for type: T.Type, id: Any) -> String {
"\(type)-\(id)"
subscript(_ index: Identifier) -> AnyWeak? {
get { indexes[index] }
set { indexes[index] = newValue }
}
}

Expand Down
32 changes: 15 additions & 17 deletions Sources/CohesionKit/Storage/EntityNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = []
var parentsRefs: Set<Identifier> = []
/// alias referencing this entity
var aliasesRefs: Set<String> = []
var aliasesRefs: Set<Identifier> = []

/// number of observers
var observersCount: Int = 0
Expand All @@ -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)
Expand Down Expand Up @@ -54,23 +53,22 @@ class EntityNode<T>: 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
private var modifiedAt: Stamp?
/// entity children
private(set) var children: [PartialKeyPath<T>: 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
Expand Down Expand Up @@ -106,15 +104,15 @@ class EntityNode<T>: AnyEntityNode {
}

func removeParent(_ node: any AnyEntityNode) {
metadata.parentsRefs.remove(node.storageKey)
metadata.parentsRefs.remove(node.id)
}

func updateEntityRelationship<U: AnyEntityNode>(_ child: U) {
guard applyChildrenChanges else {
return
}

guard let keyPath = metadata.childrenRefs[child.storageKey] else {
guard let keyPath = metadata.childrenRefs[child.id] else {
return
}

Expand Down Expand Up @@ -158,8 +156,8 @@ class EntityNode<T>: AnyEntityNode {
identity keyPath: KeyPath<T, C>,
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)
}
}
Expand Down
4 changes: 2 additions & 2 deletions Tests/CohesionKitTests/Storage/EntityNodeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,15 @@ 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() {
let childNode = EntityNode(startEntity.singleNode, modifiedAt: nil)

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 {
Expand Down