Skip to content

Commit 19e16a2

Browse files
authored
Strict main actor isolation (#138)
* Refactoring * Add a test case that unsubscription performs on background thread * Add more main actor isolation
1 parent 330123c commit 19e16a2

24 files changed

+231
-129
lines changed

Sources/Atoms/Context/AtomTransactionContext.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ public struct AtomTransactionContext: AtomWatchableContext {
77
@usableFromInline
88
internal let _store: StoreContext
99
@usableFromInline
10-
internal let _transaction: Transaction
10+
internal let _transactionState: TransactionState
1111

1212
internal init(
1313
store: StoreContext,
14-
transaction: Transaction
14+
transactionState: TransactionState
1515
) {
1616
self._store = store
17-
self._transaction = transaction
17+
self._transactionState = transactionState
1818
}
1919

2020
/// Accesses the value associated with the given atom without watching it.
@@ -191,6 +191,6 @@ public struct AtomTransactionContext: AtomWatchableContext {
191191
@inlinable
192192
@discardableResult
193193
public func watch<Node: Atom>(_ atom: Node) -> Node.Produced {
194-
_store.watch(atom, in: _transaction)
194+
_store.watch(atom, in: _transactionState)
195195
}
196196
}

Sources/Atoms/Core/AtomState.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ internal protocol AtomStateProtocol: AnyObject {
33
associatedtype Effect: AtomEffect
44

55
var effect: Effect { get }
6-
var transaction: Transaction? { get set }
6+
var transactionState: TransactionState? { get set }
77
}
88

9+
@MainActor
910
internal final class AtomState<Effect: AtomEffect>: AtomStateProtocol {
1011
let effect: Effect
11-
var transaction: Transaction?
12+
var transactionState: TransactionState?
1213

1314
init(effect: Effect) {
1415
self.effect = effect
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,43 @@
11
@MainActor
22
internal struct AtomProducerContext<Value> {
33
private let store: StoreContext
4-
private let transaction: Transaction
4+
private let transactionState: TransactionState
55
private let update: @MainActor (Value) -> Void
66

77
init(
88
store: StoreContext,
9-
transaction: Transaction,
9+
transactionState: TransactionState,
1010
update: @escaping @MainActor (Value) -> Void
1111
) {
1212
self.store = store
13-
self.transaction = transaction
13+
self.transactionState = transactionState
1414
self.update = update
1515
}
1616

1717
var isTerminated: Bool {
18-
transaction.isTerminated
18+
transactionState.isTerminated
1919
}
2020

2121
var onTermination: (@MainActor () -> Void)? {
22-
get { transaction.onTermination }
23-
nonmutating set { transaction.onTermination = newValue }
22+
get { transactionState.onTermination }
23+
nonmutating set { transactionState.onTermination = newValue }
2424
}
2525

2626
func update(with value: Value) {
2727
update(value)
2828
}
2929

3030
func transaction<T>(_ body: @MainActor (AtomTransactionContext) -> T) -> T {
31-
transaction.begin()
32-
let context = AtomTransactionContext(store: store, transaction: transaction)
33-
defer { transaction.commit() }
31+
transactionState.begin()
32+
let context = AtomTransactionContext(store: store, transactionState: transactionState)
33+
defer { transactionState.commit() }
3434
return body(context)
3535
}
3636

3737
func transaction<T>(_ body: @MainActor (AtomTransactionContext) async throws -> T) async rethrows -> T {
38-
transaction.begin()
39-
let context = AtomTransactionContext(store: store, transaction: transaction)
40-
defer { transaction.commit() }
38+
transactionState.begin()
39+
let context = AtomTransactionContext(store: store, transactionState: transactionState)
40+
defer { transactionState.commit() }
4141
return try await body(context)
4242
}
4343
}

Sources/Atoms/Core/ScopeKey.swift

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@usableFromInline
22
internal struct ScopeKey: Hashable, CustomStringConvertible {
3+
@MainActor
34
final class Token {}
45

56
private let identifier: ObjectIdentifier

Sources/Atoms/Core/StoreContext.swift

+11-11
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ internal struct StoreContext {
100100
}
101101

102102
@usableFromInline
103-
func watch<Node: Atom>(_ atom: Node, in transaction: Transaction) -> Node.Produced {
104-
guard !transaction.isTerminated else {
103+
func watch<Node: Atom>(_ atom: Node, in transactionState: TransactionState) -> Node.Produced {
104+
guard !transactionState.isTerminated else {
105105
return read(atom)
106106
}
107107

@@ -112,8 +112,8 @@ internal struct StoreContext {
112112
let value = cache?.value ?? initialize(of: atom, for: key, override: override)
113113

114114
// Add an `Edge` from the upstream to downstream.
115-
store.graph.dependencies[transaction.key, default: []].insert(key)
116-
store.graph.children[key, default: []].insert(transaction.key)
115+
store.graph.dependencies[transactionState.key, default: []].insert(key)
116+
store.graph.children[key, default: []].insert(transactionState.key)
117117

118118
return value
119119
}
@@ -189,13 +189,13 @@ internal struct StoreContext {
189189
// Restore dependencies when the refresh is completed.
190190
attachDependencies(dependencies, for: key)
191191

192-
guard let transaction = state.transaction, let cache = lookupCache(of: atom, for: key) else {
192+
guard let transactionState = state.transactionState, let cache = lookupCache(of: atom, for: key) else {
193193
checkAndRelease(for: key)
194194
return value
195195
}
196196

197197
// Notify update unless it's cancelled or terminated by other operations.
198-
if !Task.isCancelled && !transaction.isTerminated {
198+
if !Task.isCancelled && !transactionState.isTerminated {
199199
update(atom: atom, for: key, oldValue: cache.value, newValue: value)
200200
}
201201

@@ -428,7 +428,7 @@ private extension StoreContext {
428428
}
429429
}
430430

431-
state?.transaction?.terminate()
431+
state?.transactionState?.terminate()
432432

433433
let context = AtomCurrentContext(store: self)
434434
state?.effect.released(context: context)
@@ -486,7 +486,7 @@ private extension StoreContext {
486486
of atom: Node,
487487
for key: AtomKey
488488
) -> AtomProducerContext<Node.Produced> {
489-
let transaction = Transaction(key: key) {
489+
let transactionState = TransactionState(key: key) {
490490
let oldDependencies = detachDependencies(for: key)
491491

492492
return {
@@ -502,11 +502,11 @@ private extension StoreContext {
502502

503503
let state = getState(of: atom, for: key)
504504
// Terminate the ongoing transaction first.
505-
state.transaction?.terminate()
505+
state.transactionState?.terminate()
506506
// Register the transaction state so it can be terminated from anywhere.
507-
state.transaction = transaction
507+
state.transactionState = transactionState
508508

509-
return AtomProducerContext(store: self, transaction: transaction) { newValue in
509+
return AtomProducerContext(store: self, transactionState: transactionState) { newValue in
510510
if let cache = lookupCache(of: atom, for: key) {
511511
update(atom: atom, for: key, oldValue: cache.value, newValue: newValue)
512512
}

Sources/Atoms/Core/StoreState.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
internal struct StoreState {
1+
@MainActor
2+
internal final class StoreState {
23
var caches = [AtomKey: any AtomCacheProtocol]()
34
var states = [AtomKey: any AtomStateProtocol]()
45
var subscriptions = [AtomKey: [SubscriberKey: Subscription]]()

Sources/Atoms/Core/SubscriberKey.swift

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
internal struct SubscriberKey: Hashable {
2+
@MainActor
23
final class Token {}
34

45
private let identifier: ObjectIdentifier
+14-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1-
final class SubscriberState {
1+
import Foundation
2+
3+
@MainActor
4+
internal final class SubscriberState {
25
let token = SubscriberKey.Token()
36
var subscribing = Set<AtomKey>()
47
var unsubscribe: ((Set<AtomKey>) -> Void)?
58

6-
init() {}
7-
9+
// TODO: Use isolated synchronous deinit once it's available.
10+
// 0371-isolated-synchronous-deinit
811
deinit {
9-
unsubscribe?(subscribing)
12+
if Thread.isMainThread {
13+
unsubscribe?(subscribing)
14+
}
15+
else {
16+
Task(priority: .high) { @MainActor [unsubscribe, subscribing] in
17+
unsubscribe?(subscribing)
18+
}
19+
}
1020
}
1121
}

Sources/Atoms/Core/Subscription.swift

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
@usableFromInline
2-
@MainActor
32
internal struct Subscription {
43
let location: SourceLocation
5-
let update: () -> Void
4+
let update: @MainActor @Sendable () -> Void
65
}

Sources/Atoms/Core/Transaction.swift renamed to Sources/Atoms/Core/TransactionState.swift

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
@usableFromInline
22
@MainActor
3-
internal final class Transaction {
4-
private var body: (() -> () -> Void)?
5-
private var cleanup: (() -> Void)?
3+
internal final class TransactionState {
4+
private var body: (@MainActor () -> @MainActor () -> Void)?
5+
private var cleanup: (@MainActor () -> Void)?
66

77
let key: AtomKey
88

99
private var termination: (@MainActor () -> Void)?
1010
private(set) var isTerminated = false
1111

12-
init(key: AtomKey, _ body: @escaping () -> () -> Void) {
12+
init(
13+
key: AtomKey,
14+
_ body: @MainActor @escaping () -> @MainActor () -> Void
15+
) {
1316
self.key = key
1417
self.body = body
1518
}

Sources/Atoms/Core/Utilities.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
func `mutating`<T>(_ value: T, _ mutation: (inout T) -> Void) -> T {
1+
@inlinable
2+
internal func `mutating`<T>(_ value: T, _ mutation: (inout T) -> Void) -> T {
23
var value = value
34
mutation(&value)
45
return value

Sources/Atoms/Effect/InitializeEffect.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/// An atom effect that performs an arbitrary action when the atom is first used and initialized,
22
/// or once it is released and re-initialized again.
33
public struct InitializeEffect: AtomEffect {
4-
private let action: () -> Void
4+
private let action: @MainActor () -> Void
55

66
/// Creates an atom effect that performs the given action when the atom is initialized.
7-
public init(perform action: @escaping () -> Void) {
7+
public init(perform action: @MainActor @escaping () -> Void) {
88
self.action = action
99
}
1010

Sources/Atoms/Effect/MergedEffect.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// MergedEffect<each Effect: AtomEffect>
33
/// An atom effect that merges multiple atom effects into one.
44
public struct MergedEffect: AtomEffect {
5-
private let initialized: (Context) -> Void
6-
private let updated: (Context) -> Void
7-
private let released: (Context) -> Void
5+
private let initialized: @MainActor (Context) -> Void
6+
private let updated: @MainActor (Context) -> Void
7+
private let released: @MainActor (Context) -> Void
88

99
/// Creates an atom effect that merges multiple atom effects into one.
1010
public init<each Effect: AtomEffect>(_ effect: repeat each Effect) {

Sources/Atoms/Effect/ReleaseEffect.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/// An atom effect that performs an arbitrary action when the atom is no longer watched and released.
22
public struct ReleaseEffect: AtomEffect {
3-
private let action: () -> Void
3+
private let action: @MainActor () -> Void
44

55
/// Creates an atom effect that performs the given action when the atom is released.
6-
public init(perform action: @escaping () -> Void) {
6+
public init(perform action: @MainActor @escaping () -> Void) {
77
self.action = action
88
}
99

Sources/Atoms/Effect/UpdateEffect.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/// An atom effect that performs an arbitrary action when the atom is updated.
22
public struct UpdateEffect: AtomEffect {
3-
private let action: () -> Void
3+
private let action: @MainActor () -> Void
44

55
/// Creates an atom effect that performs the given action when the atom is updated.
6-
public init(perform action: @escaping () -> Void) {
6+
public init(perform action: @MainActor @escaping () -> Void) {
77
self.action = action
88
}
99

Sources/Atoms/PropertyWrapper/ViewContext.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ public struct ViewContext: DynamicProperty {
5858
subscriber: Subscriber(state.subscriberState),
5959
subscription: Subscription(
6060
location: location,
61-
update: state.objectWillChange.send
61+
update: { [weak state] in
62+
state?.objectWillChange.send()
63+
}
6264
)
6365
)
6466
}

Tests/AtomsTests/Context/AtomCurrentContextTests.swift

+14-14
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ final class AtomCurrentContextTests: XCTestCase {
1818
let atom = TestValueAtom(value: 0)
1919
let dependency = TestStateAtom(defaultValue: 100)
2020
let store = AtomStore()
21-
let transaction = Transaction(key: AtomKey(atom))
21+
let transactionState = TransactionState(key: AtomKey(atom))
2222
let storeContext = StoreContext(store: store)
2323
let context = AtomCurrentContext(store: storeContext)
2424

25-
XCTAssertEqual(storeContext.watch(dependency, in: transaction), 100)
25+
XCTAssertEqual(storeContext.watch(dependency, in: transactionState), 100)
2626

2727
context.set(200, for: dependency)
2828

29-
XCTAssertEqual(storeContext.watch(dependency, in: transaction), 200)
29+
XCTAssertEqual(storeContext.watch(dependency, in: transactionState), 200)
3030
}
3131

3232
@MainActor
@@ -58,15 +58,15 @@ final class AtomCurrentContextTests: XCTestCase {
5858
let atom = TestValueAtom(value: 0)
5959
let dependency = TestStateAtom(defaultValue: 0)
6060
let store = AtomStore()
61-
let transaction = Transaction(key: AtomKey(atom))
61+
let transactionState = TransactionState(key: AtomKey(atom))
6262
let storeContext = StoreContext(store: store)
63-
let context = AtomTransactionContext(store: StoreContext(store: store), transaction: transaction)
63+
let context = AtomTransactionContext(store: StoreContext(store: store), transactionState: transactionState)
6464

65-
XCTAssertEqual(storeContext.watch(dependency, in: transaction), 0)
65+
XCTAssertEqual(storeContext.watch(dependency, in: transactionState), 0)
6666

6767
context[dependency] = 100
6868

69-
XCTAssertEqual(storeContext.watch(dependency, in: transaction), 100)
69+
XCTAssertEqual(storeContext.watch(dependency, in: transactionState), 100)
7070

7171
context.reset(dependency)
7272

@@ -80,7 +80,7 @@ final class AtomCurrentContextTests: XCTestCase {
8080
let storeContext = StoreContext(store: store)
8181
let transactionAtom = TestValueAtom(value: 0)
8282
let atom = TestStateAtom(defaultValue: 0)
83-
let transaction = Transaction(key: AtomKey(transactionAtom))
83+
let transactionState = TransactionState(key: AtomKey(transactionAtom))
8484

8585
let resettableAtom = TestCustomResettableAtom(
8686
defaultValue: { context in
@@ -91,17 +91,17 @@ final class AtomCurrentContextTests: XCTestCase {
9191
}
9292
)
9393

94-
XCTAssertEqual(storeContext.watch(atom, in: transaction), 0)
95-
XCTAssertEqual(storeContext.watch(resettableAtom, in: transaction), 0)
94+
XCTAssertEqual(storeContext.watch(atom, in: transactionState), 0)
95+
XCTAssertEqual(storeContext.watch(resettableAtom, in: transactionState), 0)
9696

9797
context[atom] = 100
9898

99-
XCTAssertEqual(storeContext.watch(atom, in: transaction), 100)
100-
XCTAssertEqual(storeContext.watch(resettableAtom, in: transaction), 100)
99+
XCTAssertEqual(storeContext.watch(atom, in: transactionState), 100)
100+
XCTAssertEqual(storeContext.watch(resettableAtom, in: transactionState), 100)
101101

102102
context.reset(resettableAtom)
103103

104-
XCTAssertEqual(storeContext.watch(atom, in: transaction), 300)
105-
XCTAssertEqual(storeContext.watch(resettableAtom, in: transaction), 300)
104+
XCTAssertEqual(storeContext.watch(atom, in: transactionState), 300)
105+
XCTAssertEqual(storeContext.watch(resettableAtom, in: transactionState), 300)
106106
}
107107
}

0 commit comments

Comments
 (0)