Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion Sources/Atoms/AtomStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ public final class AtomStore {
internal var state = StoreState()

/// Creates a new store.
nonisolated public init() {}
public nonisolated init() {}
}
1 change: 1 addition & 0 deletions Sources/Atoms/Core/Graph.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
internal struct Graph: Equatable {
var dependencies = [AtomKey: Set<AtomKey>]()
var children = [AtomKey: Set<AtomKey>]()
var subscribed = [SubscriberKey: Set<AtomKey>]()
}
16 changes: 12 additions & 4 deletions Sources/Atoms/Core/StoreContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ internal struct StoreContext {
let (key, override) = lookupAtomKeyAndOverride(of: atom)
let cache = lookupCache(of: atom, for: key)
let value = cache?.value ?? initialize(of: atom, for: key, override: override)
let isNewSubscription = subscriber.subscribing.insert(key).inserted
let isNewSubscription = store.graph.subscribed[subscriber.key, default: []].insert(key).inserted

if isNewSubscription {
store.state.subscriptions[key, default: [:]][subscriber.key] = subscription
subscriber.unsubscribe = { keys in
unsubscribe(keys, for: subscriber.key)
subscriber.unsubscribe = {
unsubscribeAll(for: subscriber.key)
}
notifyUpdateToObservers()
}
Expand Down Expand Up @@ -209,7 +209,7 @@ internal struct StoreContext {
func unwatch(_ atom: some Atom, subscriber: Subscriber) {
let (key, _) = lookupAtomKeyAndOverride(of: atom)

subscriber.subscribing.remove(key)
store.graph.subscribed[subscriber.key]?.remove(key)
unsubscribe([key], for: subscriber.key)
}

Expand Down Expand Up @@ -462,6 +462,14 @@ private extension StoreContext {
}
}

func unsubscribeAll(for subscriberKey: SubscriberKey) {
let keys = store.graph.subscribed.removeValue(forKey: subscriberKey)

if let keys {
unsubscribe(keys, for: subscriberKey)
}
}

func unsubscribe(_ keys: some Sequence<AtomKey>, for subscriberKey: SubscriberKey) {
for key in keys {
store.state.subscriptions[key]?.removeValue(forKey: subscriberKey)
Expand Down
7 changes: 1 addition & 6 deletions Sources/Atoms/Core/Subscriber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@ internal struct Subscriber {
self.key = state.token.key
}

var subscribing: Set<AtomKey> {
get { state?.subscribing ?? [] }
nonmutating set { state?.subscribing = newValue }
}

var unsubscribe: (@MainActor (Set<AtomKey>) -> Void)? {
var unsubscribe: (@MainActor () -> Void)? {
get { state?.unsubscribe }
nonmutating set { state?.unsubscribe = newValue }
}
Expand Down
19 changes: 6 additions & 13 deletions Sources/Atoms/Core/SubscriberState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,26 @@ internal final class SubscriberState {
#endif

#if compiler(>=6)
nonisolated(unsafe) var subscribing = Set<AtomKey>()
nonisolated(unsafe) var unsubscribe: (@MainActor (Set<AtomKey>) -> Void)?
nonisolated(unsafe) var unsubscribe: (@MainActor () -> Void)?

// TODO: Use isolated synchronous deinit once it's available.
// 0371-isolated-synchronous-deinit
deinit {
MainActor.performIsolated { [unsubscribe, subscribing] in
unsubscribe?(subscribing)
MainActor.performIsolated { [unsubscribe] in
unsubscribe?()
}
}
#else
private var _subscribing = UnsafeUncheckedSendable(Set<AtomKey>())
private var _unsubscribe = UnsafeUncheckedSendable<(@MainActor (Set<AtomKey>) -> Void)?>(nil)

var subscribing: Set<AtomKey> {
_read { yield _subscribing.value }
_modify { yield &_subscribing.value }
}

var unsubscribe: (@MainActor (Set<AtomKey>) -> Void)? {
var unsubscribe: (@MainActor () -> Void)? {
_read { yield _unsubscribe.value }
_modify { yield &_unsubscribe.value }
}

deinit {
MainActor.performIsolated { [_unsubscribe, _subscribing] in
_unsubscribe.value?(_subscribing.value)
MainActor.performIsolated { [_unsubscribe] in
_unsubscribe.value?()
}
}
#endif
Expand Down
51 changes: 43 additions & 8 deletions Tests/AtomsTests/Core/StoreContextTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,17 +157,27 @@ final class StoreContextTests: XCTestCase {
)

XCTAssertEqual(context.watch(dependency0, in: transactionState), 0)
XCTAssertEqual(store.graph.dependencies, [key: [dependency0Key]])
XCTAssertEqual(store.graph.children, [dependency0Key: [key]])
XCTAssertEqual(
store.graph,
Graph(
dependencies: [key: [dependency0Key]],
children: [dependency0Key: [key]]
)
)
XCTAssertEqual((store.state.caches[dependency0Key] as? AtomCache<TestStateAtom<Int>>)?.value, 0)
XCTAssertNotNil(store.state.states[dependency0Key])
XCTAssertTrue(snapshots.flatMap(\.caches).isEmpty)

transactionState.terminate()

XCTAssertEqual(context.watch(dependency1, in: transactionState), 1)
XCTAssertEqual(store.graph.dependencies, [key: [dependency0Key]])
XCTAssertEqual(store.graph.children, [dependency0Key: [key]])
XCTAssertEqual(
store.graph,
Graph(
dependencies: [key: [dependency0Key]],
children: [dependency0Key: [key]]
)
)
XCTAssertNil(store.state.caches[dependency1Key])
XCTAssertNil(store.state.states[dependency1Key])
XCTAssertTrue(snapshots.isEmpty)
Expand Down Expand Up @@ -214,7 +224,7 @@ final class StoreContextTests: XCTestCase {
)

XCTAssertEqual(initialValue, 0)
XCTAssertTrue(subscriber.subscribing.contains(key))
XCTAssertTrue(store.graph.subscribed[subscriber.key]?.contains(key) ?? false)
XCTAssertNotNil(store.state.subscriptions[key]?[subscriber.key])
XCTAssertEqual((store.state.caches[key] as? AtomCache<TestAtom>)?.value, 0)
XCTAssertEqual((store.state.caches[dependencyKey] as? AtomCache<DependencyAtom>)?.value, 0)
Expand All @@ -226,11 +236,15 @@ final class StoreContextTests: XCTestCase {
snapshots.removeAll()
store.state.subscriptions[key]?[subscriber.key]?.update()
subscriberState = nil

XCTAssertEqual(updateCount, 1)
XCTAssertNil(store.graph.subscribed[subscriber.key])
XCTAssertNil(store.state.caches[key])
XCTAssertNil(store.state.states[key])
XCTAssertNil(store.state.subscriptions[key])
XCTAssertNil(store.state.caches[dependencyKey])
XCTAssertNil(store.state.states[dependencyKey])
XCTAssertNil(store.state.subscriptions[dependencyKey])
XCTAssertEqual(
snapshots.map { $0.caches.mapValues { $0.value as? Int } },
[[:]]
Expand Down Expand Up @@ -372,11 +386,21 @@ final class StoreContextTests: XCTestCase {
)

_ = context.watch(atom, subscriber: subscriber, subscription: Subscription())
snapshots.removeAll()
context.unwatch(atom, subscriber: subscriber)

XCTAssertEqual(
snapshots.map { $0.caches.mapValues { $0.value as? Int } },
[[:]]
[
[AtomKey(atom): 0],
[:],
]
)
XCTAssertEqual(
snapshots.map(\.graph),
[
Graph(subscribed: [subscriber.key: [AtomKey(atom)]]),
Graph(subscribed: [subscriber.key: []]),
]
)
}

Expand Down Expand Up @@ -568,7 +592,7 @@ final class StoreContextTests: XCTestCase {
context.unwatch(dependency1Atom, subscriber: subscriber)
context.unwatch(dependency2Atom, subscriber: subscriber)
scoped1Context.unwatch(dependency1Atom, subscriber: subscriber)
scoped2Context.unwatch(dependency1Atom, subscriber: subscriber)
scoped2Context.unwatch(dependency2Atom, subscriber: subscriber)

// Override for `scoped1Context` shouldn't inherited to `scoped2Context`.
XCTAssertEqual(scoped2Context.watch(atom, subscriber: subscriber, subscription: Subscription()), 21)
Expand Down Expand Up @@ -644,6 +668,12 @@ final class StoreContextTests: XCTestCase {
AtomKey(atom),
AtomKey(publisherAtom),
],
],
subscribed: [
subscriber.key: [
AtomKey((atom)),
AtomKey((publisherAtom)),
]
]
)
)
Expand Down Expand Up @@ -1014,6 +1044,11 @@ final class StoreContextTests: XCTestCase {
AtomKey(TestDependency1Atom()): [AtomKey(TestAtom())],
AtomKey(TestDependency2Atom()): [AtomKey(TestAtom())],
AtomKey(TestDependency3Atom(), scopeKey: scopeToken.key): [AtomKey(TestAtom())],
],
subscribed: [
subscriber.key: [
AtomKey(atom)
]
]
)

Expand Down
2 changes: 1 addition & 1 deletion Tests/AtomsTests/Core/SubscriberStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ final class SubscriberStateTests: XCTestCase {
var subscriberState: SubscriberState? = SubscriberState()
var unsubscribedCount = 0

subscriberState!.unsubscribe = { _ in
subscriberState!.unsubscribe = {
unsubscribedCount += 1
}

Expand Down
25 changes: 3 additions & 22 deletions Tests/AtomsTests/Core/SubscriberTestsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,42 +23,23 @@ final class SubscriberTests: XCTestCase {
)
}

@MainActor
func testSubscribing() {
var state: SubscriberState? = SubscriberState()
let subscriber = Subscriber(state!)
let expected: Set = [
AtomKey(TestAtom(value: 0)),
AtomKey(TestAtom(value: 1)),
AtomKey(TestAtom(value: 2)),
]

subscriber.subscribing = expected

XCTAssertEqual(state?.subscribing, expected)

state = nil

XCTAssertTrue(subscriber.subscribing.isEmpty)
}

@MainActor
func testUnsubscribe() {
var state: SubscriberState? = SubscriberState()
let subscriber = Subscriber(state!)
var isUnsubscribed = false

subscriber.unsubscribe = { _ in
subscriber.unsubscribe = {
isUnsubscribed = true
}

state?.unsubscribe?([])
state?.unsubscribe?()

XCTAssertTrue(isUnsubscribed)

state = nil
isUnsubscribed = false
subscriber.unsubscribe?([])
subscriber.unsubscribe?()

XCTAssertFalse(isUnsubscribed)
}
Expand Down
Loading