Skip to content
Open
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
6 changes: 4 additions & 2 deletions Sources/Sharing/Internal/Reference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,11 @@ final class _BoxReference<Value>: MutableReference, Observable, Perceptible, @un
if Thread.isMainThread {
return try _$perceptionRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
} else {
let result = try mutation()
DispatchQueue.main.async {
self._$perceptionRegistrar.withMutation(of: self, keyPath: keyPath) {}
}
return try mutation()
return result
}
#endif
}
Expand Down Expand Up @@ -339,10 +340,11 @@ final class _PersistentReference<Key: SharedReaderKey>:
if Thread.isMainThread {
return try _$perceptionRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
} else {
let result = try mutation()
DispatchQueue.main.async {
self._$perceptionRegistrar.withMutation(of: self, keyPath: keyPath) {}
}
return try mutation()
return result
}
#endif
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/Sharing/Shared.swift
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,9 @@ public struct Shared<Value> {
return self
}
nonmutating set {
reference.touch()
let oldReference = reference
reference = newValue.reference
oldReference.touch()
}
}

Expand Down
3 changes: 2 additions & 1 deletion Sources/Sharing/SharedReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,9 @@ public struct SharedReader<Value> {
return self
}
nonmutating set {
reference.touch()
let oldReference = reference
reference = newValue.reference
oldReference.touch()
}
}

Expand Down
68 changes: 68 additions & 0 deletions Tests/SharingTests/NotificationOrderingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Dispatch
import PerceptionCore
import Sharing
import Testing

#if canImport(Combine)
import Combine

@Suite struct NotificationOrderingTests {
@Test(.timeLimit(.minutes(1)))
func changeNotificationIsEnqueuedAfterBackgroundStore() async throws {
@SharedReader(wrappedValue: 0, BackgroundLoadKey(value: 1)) var value: Int
let reader = $value
try await poll(until: { reader.wrappedValue == 1 })
// NB: Drain the initial load's pending change notification before observing.
await MainActor.run {}

let events = Mutex<[String]>([])
let recordStore: @Sendable () -> Void = { events.withLock { $0.append("store") } }
let cancellable = reader.publisher
.dropFirst()
.sink { @Sendable _ in
DispatchQueue.main.async(execute: recordStore)
}
defer { _ = cancellable }

withPerceptionTracking {
_ = reader.wrappedValue
} onChange: {
events.withLock { $0.append("change") }
}

try await Task.detached { try await reader.load() }.value
try await poll(until: { events.withLock(\.count) >= 2 })

#expect(events.withLock(\.self) == ["store", "change"])
}
}

private struct BackgroundLoadKey: SharedReaderKey {
let value: Int
var id: Int { value }

func load(context: LoadContext<Int>, continuation: LoadContinuation<Int>) {
DispatchQueue.global(qos: .userInitiated).async {
continuation.resume(returning: value)
}
}

func subscribe(
context: LoadContext<Int>,
subscriber: SharedSubscriber<Int>
) -> SharedSubscription {
SharedSubscription {}
}
}

private func poll(
until condition: @escaping @Sendable () -> Bool,
timeout: Duration = .seconds(3)
) async throws {
let deadline = ContinuousClock.now + timeout
while ContinuousClock.now < deadline {
if condition() { return }
try await Task.sleep(for: .milliseconds(10))
}
}
#endif