Skip to content
Open
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
1 change: 1 addition & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"Bash(gh pr create:*)",
"mcp__swiftlens__swift_search_pattern",
"mcp__swiftlens__swift_get_symbols_overview",
"mcp__swiftlens__swift_analyze_files"
"WebFetch(domain:github.com)",
"WebFetch(domain:raw.githubusercontent.com)"
],
Expand Down
19 changes: 18 additions & 1 deletion Sources/StateGraph/StateGraph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import Foundation.NSLock
public typealias Stored<Value> = _Stored<Value, InMemoryStorage<Value>>

extension _Stored where S == InMemoryStorage<Value> {
/// 便利な初期化メソッド(wrappedValue指定)
public convenience init(
_ file: StaticString = #fileID,
_ line: UInt = #line,
Expand All @@ -37,6 +36,24 @@ extension _Stored where S == InMemoryStorage<Value> {
storage: storage
)
}

/// Equatable Filter Available
public convenience init(
_ file: StaticString = #fileID,
_ line: UInt = #line,
_ column: UInt = #column,
name: StaticString? = nil,
wrappedValue: Value
) where Value : Equatable {
let storage = InMemoryStorage(initialValue: wrappedValue)
self.init(
file,
line,
column,
name: name,
storage: storage
)
}
}

public protocol ComputedDescriptor<Value>: Sendable {
Expand Down
89 changes: 77 additions & 12 deletions Sources/StateGraph/StorageAbstraction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,21 +183,22 @@ public final class _Stored<Value, S: Storage<Value>>: Node, Observable, CustomDe
return storage.value
}
set {

lock.lock()

if let comparator {
guard comparator.isEqual(storage.value, newValue) == false else {
lock.unlock()
return
}
}

#if canImport(Observation)
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
observationRegistrar.willSet(PointerKeyPathRoot.shared, keyPath: _keyPath(self))
}

defer {
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
observationRegistrar.didSet(PointerKeyPathRoot.shared, keyPath: _keyPath(self))
}
}
#endif

lock.lock()

storage.value = newValue

let _outgoingEdges = outgoingEdges
Expand All @@ -214,9 +215,17 @@ public final class _Stored<Value, S: Storage<Value>>: Node, Observable, CustomDe
edge.isPending = true
edge.to.potentiallyDirty = true
}

#if canImport(Observation)
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
observationRegistrar.didSet(PointerKeyPathRoot.shared, keyPath: _keyPath(self))
}
#endif
}
}

private let comparator: (any _Comparator<Value>)?

private func notifyStorageUpdated() {
#if canImport(Observation)
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
Expand Down Expand Up @@ -256,23 +265,59 @@ public final class _Stored<Value, S: Storage<Value>>: Node, Observable, CustomDe

nonisolated(unsafe)
public var outgoingEdges: ContiguousArray<Edge> = []

nonisolated(unsafe)
public var trackingRegistrations: Set<TrackingRegistration> = []

public init(
public convenience init(
_ file: StaticString = #fileID,
_ line: UInt = #line,
_ column: UInt = #column,
name: StaticString? = nil,
storage: consuming S
) {
self.init(
file,
line,
column,
name: name,
storage: storage,
comparator: nil
)
}

public convenience init(
_ file: StaticString = #fileID,
_ line: UInt = #line,
_ column: UInt = #column,
name: StaticString? = nil,
storage: consuming S
) where Value : Equatable {
self.init(
file,
line,
column,
name: name,
storage: storage,
comparator: Comparator.init()
)
}

private init(
_ file: StaticString = #fileID,
_ line: UInt = #line,
_ column: UInt = #column,
name: StaticString? = nil,
storage: consuming S,
comparator: (any _Comparator<Value>)?
) {
self.info = .init(
name: name,
sourceLocation: .init(file: file, line: line, column: column)
)
self.lock = .init()
self.storage = storage
self.comparator = comparator

self.storage.loaded(context: .init(onStorageUpdated: { [weak self] in
self?.notifyStorageUpdated()
Expand Down Expand Up @@ -319,4 +364,24 @@ public final class _Stored<Value, S: Storage<Value>>: Node, Observable, CustomDe
return result
}

}
}

protocol _Comparator<Value>: Sendable {
associatedtype Value
func isEqual(_ lhs: Value, _ rhs: Value) -> Bool
}

private struct Comparator<Value: Equatable>: _Comparator, Sendable {

init() {

}

@_specialize(where Value == Int)
@_specialize(where Value == String)
@_specialize(where Value == Bool)
func isEqual(_ lhs: Value, _ rhs: Value) -> Bool {
lhs == rhs
}

}
23 changes: 3 additions & 20 deletions Tests/StateGraphTests/NodeObserveTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,7 @@ struct NodeObserveTests {
node.wrappedValue = 2
try await Task.sleep(for: .milliseconds(100))
#expect(results.withLock { $0 == [0, 1, 2] })

// Even with the same value, change notification is sent
node.wrappedValue = 2
try await Task.sleep(for: .milliseconds(100))
#expect(results.withLock { $0 == [0, 1, 2, 2] })


task.cancel()
}

Expand Down Expand Up @@ -89,13 +84,7 @@ struct NodeObserveTests {
node.wrappedValue = TestStruct(value: 2)
try await Task.sleep(for: .milliseconds(100))
#expect(results.withLock { $0 == [TestStruct(value: 0), TestStruct(value: 1), TestStruct(value: 2)] })

// Even with the same value, change notification is sent
node.wrappedValue = TestStruct(value: 2)
try await Task.sleep(for: .milliseconds(100))

#expect(results.withLock { $0 == [TestStruct(value: 0), TestStruct(value: 1), TestStruct(value: 2), TestStruct(value: 2)] })


task.cancel()
}

Expand Down Expand Up @@ -135,13 +124,7 @@ struct NodeObserveTests {
try await Task.sleep(for: .milliseconds(100))
#expect(results1.withLock { $0 == [0, 1, 2] })
#expect(results2.withLock { $0 == [0, 1, 2] })

// Even with the same value, change notification is sent
node.wrappedValue = 2
try await Task.sleep(for: .milliseconds(100))
#expect(results1.withLock { $0 == [0, 1, 2, 2] })
#expect(results2.withLock { $0 == [0, 1, 2, 2] })


task1.cancel()
task2.cancel()
}
Expand Down
Loading
Loading