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
9 changes: 9 additions & 0 deletions Sources/StateStruct/Array+modify.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

extension Array {
mutating func modify(_ modifier: (inout Element) -> Void) {
for index in indices {
modifier(&self[index])
}
}
}

30 changes: 30 additions & 0 deletions Sources/StateStruct/PropertyPath.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

public struct PropertyPath: Equatable {

public struct Component: Equatable {

public let value: String

public init(_ value: String) {
self.value = value
}

}

public var components: [Component] = []

public init() {

}

public static func root<T>(of type: T.Type) -> PropertyPath {
let path = PropertyPath().pushed(.init(_typeName(type)))
return path
}

public consuming func pushed(_ component: Component) -> PropertyPath {
self.components.append(component)
return self
}

}
161 changes: 1 addition & 160 deletions Sources/StateStruct/Tracking.swift
Original file line number Diff line number Diff line change
@@ -1,173 +1,14 @@
import Foundation
import os.lock

extension Array {
mutating func modify(_ modifier: (inout Element) -> Void) {
for index in indices {
modifier(&self[index])
}
}
}


public final class _TrackingContext: Sendable, Hashable {

public static func == (lhs: _TrackingContext, rhs: _TrackingContext) -> Bool {
// ``_TrackingContext`` is used only for embedding into the struct.
// It always returns true when checked for equality to prevent
// interfering with the actual equality check of the struct.
return true
}

public func hash(into hasher: inout Hasher) {
0.hash(into: &hasher)
}

@inlinable
public var path: PropertyPath? {
get {
infoBox.withLockUnchecked {
$0[Unmanaged.passUnretained(Thread.current).toOpaque()]?.path
}
}
set {
infoBox.withLockUnchecked {
$0[Unmanaged.passUnretained(Thread.current).toOpaque(), default: .init()].path = newValue
}
}
}

public var identifier: AnyHashable? {
get {
infoBox.withLockUnchecked {
$0[Unmanaged.passUnretained(Thread.current).toOpaque()]?.identifier
}
}
set {
infoBox.withLockUnchecked {
$0[Unmanaged.passUnretained(Thread.current).toOpaque(), default: .init()].identifier = newValue
}
}
}

@usableFromInline
struct Info {
@usableFromInline
var path: PropertyPath?

@usableFromInline
var identifier: AnyHashable?

@inlinable
init() {
}
}

@usableFromInline
let infoBox: OSAllocatedUnfairLock<[UnsafeMutableRawPointer: Info]> = .init(
uncheckedState: [:]
)

public init() {
}
}

public struct PropertyPath: Equatable {

public struct Component: Equatable {

public let value: String

public init(_ value: String) {
self.value = value
}

}

public var components: [Component] = []

public init() {

}

public static func root<T>(of type: T.Type) -> PropertyPath {
let path = PropertyPath().pushed(.init(_typeName(type)))
return path
}

public consuming func pushed(_ component: Component) -> PropertyPath {
self.components.append(component)
return self
}

}

extension TrackingObject {

public func tracking(
using graph: consuming PropertyNode? = nil,
_ applier: () throws -> Void
) rethrows -> TrackingResult {
let current = Thread.current.threadDictionary.tracking
startTracking()
defer {
Thread.current.threadDictionary.tracking = current
endTracking()
}

Thread.current.threadDictionary.tracking = TrackingResult(graph: graph ?? .init(name: _typeName(type(of: self))))
try applier()
let result = Thread.current.threadDictionary.tracking!
return result
}

private func startTracking() {
_tracking_context.path = .root(of: Self.self)
}

private func endTracking() {
}

}

public struct TrackingResult: Equatable {

public var graph: PropertyNode

public init(graph: consuming PropertyNode) {
self.graph = graph
}

public mutating func accessorRead(path: PropertyPath?) {
guard let path = path else {
return
}
graph.applyAsRead(path: path)
}

public mutating func accessorSet(path: PropertyPath?) {
guard let path = path else {
return
}
graph.applyAsWrite(path: path)
}

public mutating func accessorModify(path: PropertyPath?) {
guard let path = path else {
return
}
graph.applyAsWrite(path: path)
}
}

private enum ThreadDictionaryKey {
case tracking
case currentKeyPathStack
}

extension NSMutableDictionary {

fileprivate var tracking: TrackingResult? {
var tracking: TrackingResult? {
get {
self[ThreadDictionaryKey.tracking] as? TrackingResult
}
Expand Down
30 changes: 30 additions & 0 deletions Sources/StateStruct/TrackingObject.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
import Foundation.NSThread

/// A type that represents an object that can be tracked for changes.
/// This protocol is automatically implemented by types marked with `@Tracking` macro.
public protocol TrackingObject {
var _tracking_context: _TrackingContext { get }
}

extension TrackingObject {

public func tracking(
using graph: consuming PropertyNode? = nil,
_ applier: () throws -> Void
) rethrows -> TrackingResult {
let current = Thread.current.threadDictionary.tracking
startTracking()
defer {
Thread.current.threadDictionary.tracking = current
endTracking()
}

Thread.current.threadDictionary.tracking = TrackingResult(graph: graph ?? .init(name: _typeName(type(of: self))))
try applier()
let result = Thread.current.threadDictionary.tracking!
return result
}

private func startTracking() {
_tracking_context.path = .root(of: Self.self)
}

private func endTracking() {
}

}
40 changes: 40 additions & 0 deletions Sources/StateStruct/TrackingResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@

public final class TrackingResultRef {

public var result: TrackingResult

public init(result: TrackingResult) {
self.result = result
}

}

public struct TrackingResult: Equatable {

public var graph: PropertyNode

public init(graph: consuming PropertyNode) {
self.graph = graph
}

public mutating func accessorRead(path: PropertyPath?) {
guard let path = path else {
return
}
graph.applyAsRead(path: path)
}

public mutating func accessorSet(path: PropertyPath?) {
guard let path = path else {
return
}
graph.applyAsWrite(path: path)
}

public mutating func accessorModify(path: PropertyPath?) {
guard let path = path else {
return
}
graph.applyAsWrite(path: path)
}
}
64 changes: 64 additions & 0 deletions Sources/StateStruct/_TrackingContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import os.lock
import Foundation.NSThread

public final class _TrackingContext: Sendable, Hashable {

public static func == (lhs: _TrackingContext, rhs: _TrackingContext) -> Bool {
// ``_TrackingContext`` is used only for embedding into the struct.
// It always returns true when checked for equality to prevent
// interfering with the actual equality check of the struct.
return true
}

public func hash(into hasher: inout Hasher) {
0.hash(into: &hasher)
}

@inlinable
public var path: PropertyPath? {
get {
infoBox.withLockUnchecked {
$0[Unmanaged.passUnretained(Thread.current).toOpaque()]?.path
}
}
set {
infoBox.withLockUnchecked {
$0[Unmanaged.passUnretained(Thread.current).toOpaque(), default: .init()].path = newValue
}
}
}

public var identifier: AnyHashable? {
get {
infoBox.withLockUnchecked {
$0[Unmanaged.passUnretained(Thread.current).toOpaque()]?.identifier
}
}
set {
infoBox.withLockUnchecked {
$0[Unmanaged.passUnretained(Thread.current).toOpaque(), default: .init()].identifier = newValue
}
}
}

@usableFromInline
struct Info {
@usableFromInline
var path: PropertyPath?

@usableFromInline
var identifier: AnyHashable?

@inlinable
init() {
}
}

@usableFromInline
let infoBox: OSAllocatedUnfairLock<[UnsafeMutableRawPointer: Info]> = .init(
uncheckedState: [:]
)

public init() {
}
}