Skip to content
This repository was archived by the owner on Aug 22, 2025. It is now read-only.

Commit 093659f

Browse files
authored
Decision/decision environment transactions (#45)
1 parent e6cc476 commit 093659f

File tree

16 files changed

+100
-622
lines changed

16 files changed

+100
-622
lines changed

Decide-Tests/SwiftUI_Tests.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import SwiftUI
1616
import Decide
1717
import XCTest
18-
import DecideTesting
1918

2019
@MainActor final class SwiftUI_Tests: XCTestCase {
2120

@@ -33,8 +32,8 @@ import DecideTesting
3332
struct UpdateStr: ValueDecision {
3433
var newValue: String
3534

36-
func mutate(_ env: Decide.DecisionEnvironment) {
37-
// env[\.Storage.$str] = newValue
35+
func mutate(_ env: DecisionEnvironment) {
36+
var x = env[\Storage.$str]
3837
}
3938
}
4039

Decide/Binding/Bind.swift

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,16 @@ public final class Bind<Root, Value> where Root: StateRoot {
4747
get {
4848
let wrapperInstance = instance[keyPath: storageKeyPath]
4949
let root = wrapperInstance.environment.get(Root.self)
50-
let observableValue = root[keyPath: wrapperInstance.statePath]
51-
52-
#warning("""
53-
TODO: Squash updates of any values this instance is subscribed to,
54-
to one update to instance.
55-
""")
56-
let observer = Observer(wrapperInstance) { [weak instance] in
50+
let observableValue = root[keyPath: wrapperInstance.statePath.appending(path: \.storage)]
51+
let observer = Observer(instance) { [weak instance] in
5752
instance?.onChange()
5853
}
59-
60-
6154
return observableValue.getValueSubscribing(observer: observer)
6255
}
6356
set {
6457
let wrapperInstance = instance[keyPath: storageKeyPath]
6558
let root = wrapperInstance.environment.get(Root.self)
66-
let observableValue = root[keyPath: wrapperInstance.statePath]
59+
let observableValue = root[keyPath: wrapperInstance.statePath.appending(path: \.storage)]
6760
observableValue.set(value: newValue)
6861
}
6962
}

Decide/Binding/SwiftUIBinding.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public struct SwiftUIBind<
3131
public var wrappedValue: Value {
3232
get {
3333
environment
34-
.get(Root.self)[keyPath: statePath]
34+
.get(Root.self)[keyPath: statePath.appending(path: \.storage)]
3535
.getValueSubscribing(
3636
observer: Observer(publisher) { [weak publisher] in
3737
publisher?.objectWillChange.send()
@@ -40,7 +40,7 @@ public struct SwiftUIBind<
4040
}
4141
nonmutating set {
4242
environment
43-
.get(Root.self)[keyPath: statePath]
43+
.get(Root.self)[keyPath: statePath.appending(path: \.storage)]
4444
.set(value: newValue)
4545
}
4646
}

Decide/Decision/Decision.swift

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,38 +51,76 @@ public typealias EnvironmentMutation = (DecisionEnvironment) -> Void
5151
}
5252

5353
func make(decision: Decision) {
54-
/**
55-
1. Create a temporary environment, something to enable transactions
56-
2. Perform that mutation
57-
3. Execute effects
58-
*/
5954
decision.mutate(self)
55+
observers.forEach { $0.send() }
56+
performEffects()
57+
observers = []
6058
}
6159

62-
func perform(effect: Effect) {
63-
60+
func performEffects() {
61+
62+
}
63+
64+
private var observers: Set<Observer> = []
65+
66+
func getValue<Root: StateRoot, Value>(_ path: KeyPath<Root, ValueStorage<Value>>) -> Value {
67+
let root = environment.get(Root.self)
68+
let observableValue = root[keyPath: path]
69+
return observableValue.value
70+
}
71+
72+
func set<Root: StateRoot, Value>(value newValue: Value, path: KeyPath<Root, ValueStorage<Value>>) {
73+
let root = environment.get(Root.self)
74+
let observableValue = root[keyPath: path]
75+
// We pop all observers of the observable values mutated by the decision.
76+
// it also will prevent their observers to be notified
77+
// until decision application is complete.
78+
let valueObservers = observableValue.observation.popObservers()
79+
// then we add them into set of all the observers that we will notify
80+
// when all states affected by decision are mutated.
81+
// this way we squash all the updates to:
82+
// Each observer notified **once**,
83+
// regardless how many values were updated.
84+
// E.g. some object observes `A.a` `A.b` and `B.a` where A and B are roots.
85+
// and .a .b are values.
86+
// Decision updates all of them, but since it's a single observer (Observer.id),
87+
// it will only get one update.
88+
observers.formUnion(valueObservers)
89+
observableValue.value = newValue
6490
}
6591

6692
/**
6793
Subscript to direct read/write access to any atomic state.
6894
*/
69-
public subscript<Root: StateRoot, Value>(_ path: KeyPath<Root, ObservableValue<Value>>) -> Value {
70-
get {
71-
let root = environment.get(Root.self)
72-
let observableValue = root[keyPath: path]
73-
return observableValue.wrappedValue
74-
}
75-
set {
76-
let root = environment.get(Root.self)
77-
let observableValue = root[keyPath: path]
78-
observableValue.set(value: newValue)
79-
}
95+
public subscript<
96+
Root: StateRoot,
97+
Value
98+
>(
99+
_ path: KeyPath<Root, ObservableValue<Value>>
100+
) -> Value {
101+
get { getValue(path.appending(path: \.storage)) }
102+
set { set(value: newValue, path: path.appending(path: \.storage)) }
103+
}
104+
105+
public subscript<
106+
Root: StateRoot,
107+
Wrapper: ObservableValueStorageWrapper,
108+
Value
109+
>(
110+
path: KeyPath<Root, Wrapper>
111+
) -> Value where Wrapper.Value == Value {
112+
get { getValue(path.appending(path: \.storage)) }
113+
set { set(value: newValue, path: path.appending(path: \.storage)) }
80114
}
81115

82116
/**
83117
Subscript to direct read/write access to any identified state.
84118
*/
85-
public subscript<Identifier: Hashable, Root: IdentifiedStateRoot, Value>(
119+
public subscript<
120+
Identifier: Hashable,
121+
Root: IdentifiedStateRoot,
122+
Value
123+
>(
86124
_ path: KeyPath<Root, ObservableValue<Value>>,
87125
at id: Identifier
88126
) -> Value where Root.Identifier == Identifier {
@@ -93,7 +131,7 @@ public typealias EnvironmentMutation = (DecisionEnvironment) -> Void
93131
}
94132
set {
95133
let root = environment.get(Root.self, at: id)
96-
let observableValue = root[keyPath: path]
134+
let observableValue = root[keyPath: path.appending(path: \.storage)]
97135
observableValue.set(value: newValue)
98136
}
99137
}

Decide/Environment/Observability.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ import Foundation
1616

1717
/// Holds a week reference to actual observer, notifies only if object still exist.
1818
public final class Observer: Hashable {
19-
private(set) var notify: () -> Void
19+
private var notify: () -> Void
2020
private var id: ObjectIdentifier
2121

22+
public func send() {
23+
print("notified \(id)")
24+
notify()
25+
}
26+
2227
@MainActor init<O: AnyObject>(_ observer: O, notify: @escaping () -> Void) {
2328
self.notify = notify
2429
self.id = ObjectIdentifier(observer)
@@ -51,7 +56,7 @@ public final class Observer: Hashable {
5156

5257
func sendAll() {
5358
let observers = popObservers()
54-
observers.forEach { $0.notify() }
59+
observers.forEach { $0.send() }
5560
}
5661
}
5762

Decide/Macros.swift

Lines changed: 0 additions & 15 deletions
This file was deleted.

Decide/Persistency/Persistency.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ import Foundation
2121
*/
2222
@propertyWrapper
2323
@MainActor
24-
public final class Persistent<Value>: ObservableValueWrapper {
24+
public final class Persistent<Value>: ObservableValueStorageWrapper {
2525
public var wrappedValue: Value { storage.value }
26+
public var projectedValue: Persistent<Value> { self }
2627
public var storage: ValueStorage<Value>
2728

2829
public init(wrappedValue: @autoclosure @escaping () -> Value) {
2930
self.storage = ValueStorage(initialValue: wrappedValue)
3031
}
3132

32-
public init<Wrapper: ObservableValueWrapper>(wrappedValue: Wrapper) where Wrapper.Value == Value {
33+
public init<Wrapper: ObservableValueStorageWrapper>(wrappedValue: Wrapper) where Wrapper.Value == Value {
3334
self.storage = wrappedValue.storage
3435
}
3536
}

Decide/RemoteValue/RemoteSync.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,16 @@ import Foundation
2222
*/
2323
@propertyWrapper
2424
@MainActor
25-
public final class RemoteValue<Value>: ObservableValueWrapper {
25+
public final class RemoteValue<Value>: ObservableValueStorageWrapper {
2626
public var wrappedValue: Value { storage.value }
27+
public var projectedValue: RemoteValue<Value> { self }
2728
public var storage: ValueStorage<Value>
2829

2930
public init(wrappedValue: @autoclosure @escaping () -> Value) {
3031
self.storage = ValueStorage(initialValue: wrappedValue)
3132
}
3233

33-
public init<Wrapper: ObservableValueWrapper>(wrappedValue: Wrapper) where Wrapper.Value == Value {
34+
public init<Wrapper: ObservableValueStorageWrapper>(wrappedValue: Wrapper) where Wrapper.Value == Value {
3435
self.storage = wrappedValue.storage
3536
}
3637
}

Decide/State/ObservableValue.swift

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
Contains the reference to the storage of the value.
1818
*/
1919
@MainActor
20-
public protocol ObservableValueWrapper {
20+
public protocol ObservableValueStorageWrapper {
2121
associatedtype Value
2222
var storage: ValueStorage<Value> { get }
23+
var projectedValue: Self { get }
2324
}
2425

2526
/**
@@ -28,35 +29,20 @@ public protocol ObservableValueWrapper {
2829
*/
2930
@propertyWrapper
3031
@MainActor
31-
public final class ObservableValue<Value> {
32-
32+
public final class ObservableValue<Value>: ObservableValueStorageWrapper {
3333
public var wrappedValue: Value {
34-
valueStorage.value
34+
storage.value
3535
}
3636

3737
public var projectedValue: ObservableValue<Value> { self }
3838

39-
var valueStorage: ValueStorage<Value>
40-
var observation = ObserverStorage()
39+
public var storage: ValueStorage<Value>
4140

4241
public init(wrappedValue: @autoclosure @escaping () -> Value) {
43-
self.valueStorage = ValueStorage(initialValue: wrappedValue)
42+
self.storage = ValueStorage(initialValue: wrappedValue)
4443
}
4544

46-
public init<Wrapper: ObservableValueWrapper>(wrappedValue: Wrapper) where Wrapper.Value == Value {
47-
self.valueStorage = wrappedValue.storage
45+
public init<Wrapper: ObservableValueStorageWrapper>(wrappedValue: Wrapper) where Wrapper.Value == Value {
46+
self.storage = wrappedValue.storage
4847
}
4948
}
50-
51-
extension ObservableValue {
52-
public func getValueSubscribing(observer: Observer) -> Value {
53-
observation.subscribe(observer)
54-
return wrappedValue
55-
}
56-
57-
public func set(value newValue: Value) {
58-
valueStorage.value = newValue
59-
observation.sendAll()
60-
}
61-
}
62-

Decide/State/ValueStorage.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Foundation
1616

1717
@MainActor
1818
public final class ValueStorage<Value> {
19+
var observation = ObserverStorage()
1920
public var initialValue: () -> Value
2021
public var value: Value {
2122
get {
@@ -39,3 +40,15 @@ public final class ValueStorage<Value> {
3940
self.initialValue = initialValue
4041
}
4142
}
43+
44+
extension ValueStorage {
45+
public func getValueSubscribing(observer: Observer) -> Value {
46+
observation.subscribe(observer)
47+
return value
48+
}
49+
50+
public func set(value newValue: Value) {
51+
value = newValue
52+
observation.sendAll()
53+
}
54+
}

0 commit comments

Comments
 (0)