Skip to content

Commit d9a42ba

Browse files
authored
Detach atom dependencies while executing custom refresh (#132)
1 parent 9471292 commit d9a42ba

File tree

8 files changed

+138
-71
lines changed

8 files changed

+138
-71
lines changed

Sources/Atoms/Core/StoreContext.swift

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,15 @@ internal struct StoreContext {
180180
let key = AtomKey(atom, scopeKey: scopeKey)
181181
let state = getState(of: atom, for: key)
182182
let context = AtomCurrentContext(store: self, coordinator: state.coordinator)
183+
184+
// Detach the dependencies once to delay updating the downstream until
185+
// this atom's value refresh is complete.
186+
let dependencies = detachDependencies(for: key)
183187
let value = await atom.refresh(context: context)
184188

189+
// Restore dependencies when the refresh is completed.
190+
attachDependencies(dependencies, for: key)
191+
185192
guard let transaction = state.transaction, let cache = lookupCache(of: atom, for: key) else {
186193
checkAndRelease(for: key)
187194
return value
@@ -454,6 +461,28 @@ private extension StoreContext {
454461
release(for: key)
455462
}
456463

464+
func detachDependencies(for key: AtomKey) -> Set<AtomKey> {
465+
// Remove current dependencies.
466+
let dependencies = store.graph.dependencies.removeValue(forKey: key) ?? []
467+
468+
// Detatch the atom from its children.
469+
for dependency in dependencies {
470+
store.graph.children[dependency]?.remove(key)
471+
}
472+
473+
return dependencies
474+
}
475+
476+
func attachDependencies(_ dependencies: Set<AtomKey>, for key: AtomKey) {
477+
// Set dependencies.
478+
store.graph.dependencies[key] = dependencies
479+
480+
// Attach the atom to its children.
481+
for dependency in dependencies {
482+
store.graph.children[dependency]?.insert(key)
483+
}
484+
}
485+
457486
func unsubscribe<Keys: Sequence<AtomKey>>(_ keys: Keys, for subscriberKey: SubscriberKey) {
458487
for key in keys {
459488
store.state.subscriptions[key]?.removeValue(forKey: subscriberKey)
@@ -468,13 +497,7 @@ private extension StoreContext {
468497
for key: AtomKey
469498
) -> AtomProducerContext<Node.Produced, Node.Coordinator> {
470499
let transaction = Transaction(key: key) {
471-
// Remove current dependencies.
472-
let oldDependencies = store.graph.dependencies.removeValue(forKey: key) ?? []
473-
474-
// Detatch the atom from its children.
475-
for dependency in oldDependencies {
476-
store.graph.children[dependency]?.remove(key)
477-
}
500+
let oldDependencies = detachDependencies(for: key)
478501

479502
return {
480503
let dependencies = store.graph.dependencies[key] ?? []
Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import Combine
21
import XCTest
32

43
@testable import Atoms
@@ -9,10 +8,10 @@ final class RefreshableTests: XCTestCase {
98
let store = AtomStore()
109
let subscriberState = SubscriberState()
1110
let subscriber = Subscriber(subscriberState)
12-
let atom = TestCustomRefreshableAtom {
13-
Just(0)
14-
} refresh: {
15-
.success(1)
11+
let atom = TestCustomRefreshableAtom { _ in
12+
0
13+
} refresh: { _ in
14+
1
1615
}
1716
let key = AtomKey(atom)
1817
var snapshots = [Snapshot]()
@@ -22,33 +21,32 @@ final class RefreshableTests: XCTestCase {
2221
do {
2322
// Should call custom refresh behavior
2423

25-
let phase0 = await context.refresh(atom)
26-
XCTAssertEqual(phase0.value, 1)
24+
let value0 = await context.refresh(atom)
25+
XCTAssertEqual(value0, 1)
2726
XCTAssertNil(store.state.caches[key])
2827
XCTAssertNil(store.state.states[key])
2928
XCTAssertTrue(snapshots.isEmpty)
3029

3130
var updateCount = 0
32-
let phase1 = context.watch(
31+
let value1 = context.watch(
3332
atom,
3433
subscriber: subscriber,
3534
subscription: Subscription {
3635
updateCount += 1
3736
}
3837
)
3938

40-
XCTAssertTrue(phase1.isSuspending)
39+
XCTAssertEqual(value1, 0)
4140

4241
snapshots.removeAll()
43-
44-
let phase2 = await context.refresh(atom)
45-
XCTAssertEqual(phase2.value, 1)
42+
let value2 = await context.refresh(atom)
43+
XCTAssertEqual(value2, 1)
4644
XCTAssertNotNil(store.state.states[key])
47-
XCTAssertEqual((store.state.caches[key] as? AtomCache<TestCustomRefreshableAtom<Just<Int>>>)?.value, .success(1))
45+
XCTAssertEqual((store.state.caches[key] as? AtomCache<TestCustomRefreshableAtom<Int>>)?.value, 1)
4846
XCTAssertEqual(updateCount, 1)
4947
XCTAssertEqual(
50-
snapshots.map { $0.caches.mapValues { $0.value as? AsyncPhase<Int, Never> } },
51-
[[key: .success(1)]]
48+
snapshots.map { $0.caches.mapValues { $0.value as? Int } },
49+
[[key: 1]]
5250
)
5351

5452
context.unwatch(atom, subscriber: subscriber)
@@ -64,30 +62,58 @@ final class RefreshableTests: XCTestCase {
6462
scopeID: ScopeID(DefaultScopeID()),
6563
observers: [],
6664
overrides: [
67-
OverrideKey(atom): Override<TestCustomRefreshableAtom<Just<Int>>>(isScoped: true) { _ in .success(2) }
65+
OverrideKey(atom): Override<TestCustomRefreshableAtom<Int>>(isScoped: true) { _ in 2 }
6866
]
6967
)
7068

71-
let phase0 = scopedContext.watch(atom, subscriber: subscriber, subscription: Subscription())
72-
XCTAssertEqual(phase0.value, 2)
69+
let value0 = scopedContext.watch(atom, subscriber: subscriber, subscription: Subscription())
70+
XCTAssertEqual(value0, 2)
7371

74-
let phase1 = await scopedContext.refresh(atom)
75-
XCTAssertEqual(phase1.value, 1)
72+
let value1 = await scopedContext.refresh(atom)
73+
XCTAssertEqual(value1, 1)
7674
XCTAssertNotNil(store.state.states[overrideAtomKey])
7775
XCTAssertEqual(
78-
(store.state.caches[overrideAtomKey] as? AtomCache<TestCustomRefreshableAtom<Just<Int>>>)?.value,
79-
.success(1)
76+
(store.state.caches[overrideAtomKey] as? AtomCache<TestCustomRefreshableAtom<Int>>)?.value,
77+
1
8078
)
8179
}
8280

8381
do {
8482
// Should not make new state and cache
8583

86-
let phase = await context.refresh(atom)
84+
let value = await context.refresh(atom)
8785

88-
XCTAssertEqual(phase.value, 1)
86+
XCTAssertEqual(value, 1)
8987
XCTAssertNil(store.state.states[key])
9088
XCTAssertNil(store.state.caches[key])
9189
}
9290
}
91+
92+
@MainActor
93+
func testTransitiveRefresh() async {
94+
let parentAtom = TestTaskAtom { 0 }
95+
let atom = TestCustomRefreshableAtom { context in
96+
context.watch(parentAtom.phase)
97+
} refresh: { context in
98+
await context.refresh(parentAtom.phase)
99+
}
100+
let context = AtomTestContext()
101+
102+
var updateCount = 0
103+
context.onUpdate = {
104+
updateCount += 1
105+
}
106+
107+
XCTAssertTrue(context.watch(atom).isSuspending)
108+
109+
await context.waitForUpdate()
110+
XCTAssertEqual(context.watch(atom).value, 0)
111+
XCTAssertEqual(updateCount, 1)
112+
113+
let value = await context.refresh(atom).value
114+
115+
XCTAssertEqual(value, 0)
116+
XCTAssertEqual(context.watch(atom).value, 0)
117+
XCTAssertEqual(updateCount, 2)
118+
}
93119
}

Tests/AtomsTests/Attribute/ResettableTests.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,28 @@ final class ResettableTests: XCTestCase {
9999
XCTAssertNil(store.state.caches[key])
100100
}
101101
}
102+
103+
@MainActor
104+
func testTransitiveReset() {
105+
let parentAtom = TestValueAtom(value: 0)
106+
let atom = TestCustomResettableAtom { context in
107+
context.watch(parentAtom)
108+
} reset: { context in
109+
context.reset(parentAtom)
110+
}
111+
let context = AtomTestContext()
112+
113+
var updateCount = 0
114+
context.onUpdate = {
115+
updateCount += 1
116+
}
117+
118+
XCTAssertEqual(context.watch(atom), 0)
119+
XCTAssertEqual(updateCount, 0)
120+
121+
context.reset(atom)
122+
123+
XCTAssertEqual(context.watch(atom), 0)
124+
XCTAssertEqual(updateCount, 1)
125+
}
102126
}

Tests/AtomsTests/Context/AtomCurrentContextTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ final class AtomCurrentContextTests: XCTestCase {
4141

4242
@MainActor
4343
func testCustomRefresh() async {
44-
let atom = TestCustomRefreshableAtom {
45-
Just(100)
46-
} refresh: {
47-
.success(200)
44+
let atom = TestCustomRefreshableAtom { _ in
45+
100
46+
} refresh: { _ in
47+
200
4848
}
4949
let store = AtomStore()
5050
let context = AtomCurrentContext(store: StoreContext(store: store), coordinator: ())
51-
let value = await context.refresh(atom).value
5251

52+
let value = await context.refresh(atom)
5353
XCTAssertEqual(value, 200)
5454
}
5555

Tests/AtomsTests/Context/AtomTestContextTests.swift

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,10 @@ final class AtomTestContextTests: XCTestCase {
211211

212212
@MainActor
213213
func testCustomRefresh() async {
214-
let atom = TestCustomRefreshableAtom {
215-
Just(100)
216-
} refresh: {
217-
.success(200)
214+
let atom = TestCustomRefreshableAtom { _ in
215+
100
216+
} refresh: { _ in
217+
200
218218
}
219219
let context = AtomTestContext()
220220
var updateCount = 0
@@ -223,17 +223,13 @@ final class AtomTestContextTests: XCTestCase {
223223
updateCount += 1
224224
}
225225

226-
XCTAssertTrue(context.watch(atom).isSuspending)
227-
228-
await context.waitForUpdate()
229-
230-
XCTAssertEqual(context.watch(atom).value, 100)
226+
XCTAssertEqual(context.watch(atom), 100)
231227

232-
let value = await context.refresh(atom).value
228+
let value = await context.refresh(atom)
233229

234230
XCTAssertEqual(value, 200)
235-
XCTAssertEqual(context.watch(atom).value, 200)
236-
XCTAssertEqual(updateCount, 2)
231+
XCTAssertEqual(context.watch(atom), 200)
232+
XCTAssertEqual(updateCount, 1)
237233
}
238234

239235
@MainActor

Tests/AtomsTests/Context/AtomTransactionContextTests.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,20 @@ final class AtomTransactionContextTests: XCTestCase {
4848
@MainActor
4949
func testCustomRefresh() async {
5050
let atom0 = TestValueAtom(value: 0)
51-
let atom1 = TestCustomRefreshableAtom {
52-
Just(100)
53-
} refresh: {
54-
.success(200)
51+
let atom1 = TestCustomRefreshableAtom { _ in
52+
100
53+
} refresh: { _ in
54+
200
5555
}
5656
let store = AtomStore()
5757
let transaction = Transaction(key: AtomKey(atom0))
5858
let context = AtomTransactionContext(store: StoreContext(store: store), transaction: transaction, coordinator: ())
5959

60-
XCTAssertTrue(context.watch(atom1).isSuspending)
61-
62-
let value = await context.refresh(atom1).value
60+
XCTAssertEqual(context.watch(atom1), 100)
6361

62+
let value = await context.refresh(atom1)
6463
XCTAssertEqual(value, 200)
65-
XCTAssertEqual(context.watch(atom1).value, 200)
64+
XCTAssertEqual(context.watch(atom1), 200)
6665
}
6766

6867
@MainActor

Tests/AtomsTests/Context/AtomViewContextTests.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ final class AtomViewContextTests: XCTestCase {
5757

5858
@MainActor
5959
func testCustomRefresh() async {
60-
let atom = TestCustomRefreshableAtom {
61-
Just(100)
62-
} refresh: {
63-
.success(200)
60+
let atom = TestCustomRefreshableAtom { _ in
61+
100
62+
} refresh: { _ in
63+
200
6464
}
6565
let store = AtomStore()
6666
let subscriberState = SubscriberState()
@@ -70,12 +70,11 @@ final class AtomViewContextTests: XCTestCase {
7070
subscription: Subscription()
7171
)
7272

73-
XCTAssertTrue(context.watch(atom).isSuspending)
74-
75-
let value = await context.refresh(atom).value
73+
XCTAssertEqual(context.watch(atom), 100)
7674

75+
let value = await context.refresh(atom)
7776
XCTAssertEqual(value, 200)
78-
XCTAssertEqual(context.watch(atom).value, 200)
77+
XCTAssertEqual(context.watch(atom), 200)
7978
}
8079

8180
@MainActor

Tests/AtomsTests/Utilities/TestAtom.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,20 @@ struct TestThrowingTaskAtom<Success: Sendable>: ThrowingTaskAtom {
7878
}
7979
}
8080

81-
struct TestCustomRefreshableAtom<Publisher: Combine.Publisher>: PublisherAtom, Refreshable {
82-
var makePublisher: () -> Publisher
83-
var refresh: () -> AsyncPhase<Publisher.Output, Publisher.Failure>
81+
struct TestCustomRefreshableAtom<T>: ValueAtom, Refreshable {
82+
var getValue: (Context) -> T
83+
var refresh: (CurrentContext) async -> T
8484

8585
var key: UniqueKey {
8686
UniqueKey()
8787
}
8888

89-
func publisher(context: Context) -> Publisher {
90-
makePublisher()
89+
func value(context: Context) -> T {
90+
getValue(context)
9191
}
9292

93-
func refresh(context: CurrentContext) async -> AsyncPhase<Publisher.Output, Publisher.Failure> {
94-
refresh()
93+
func refresh(context: CurrentContext) async -> T {
94+
await refresh(context)
9595
}
9696
}
9797

0 commit comments

Comments
 (0)