mirror of
https://github.com/samsonjs/AsyncMonitor.git
synced 2026-03-25 08:25:47 +00:00
Add support for iOS 17 and macOS 14
This commit is contained in:
parent
77130baceb
commit
ef4083dc64
5 changed files with 92 additions and 17 deletions
|
|
@ -5,8 +5,8 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "AsyncMonitor",
|
name: "AsyncMonitor",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v18),
|
.iOS(.v17),
|
||||||
.macOS(.v15),
|
.macOS(.v14),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ public final class AsyncMonitor: Hashable, AsyncCancellable {
|
||||||
/// Defaults to `#isolation`, preserving the caller's actor isolation.
|
/// Defaults to `#isolation`, preserving the caller's actor isolation.
|
||||||
/// - sequence: The asynchronous sequence of elements to observe.
|
/// - sequence: The asynchronous sequence of elements to observe.
|
||||||
/// - block: A closure to execute for each element yielded by the sequence.
|
/// - block: A closure to execute for each element yielded by the sequence.
|
||||||
|
@available(iOS 18, *)
|
||||||
public init<Element: Sendable>(
|
public init<Element: Sendable>(
|
||||||
isolation: isolated (any Actor)? = #isolation,
|
isolation: isolated (any Actor)? = #isolation,
|
||||||
sequence: any AsyncSequence<Element, Never>,
|
sequence: any AsyncSequence<Element, Never>,
|
||||||
|
|
@ -34,6 +35,29 @@ public final class AsyncMonitor: Hashable, AsyncCancellable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates an ``AsyncMonitor`` that observes the provided asynchronous sequence.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - sequence: The asynchronous sequence of elements to observe.
|
||||||
|
/// - block: A closure to execute for each element yielded by the sequence.
|
||||||
|
@available(iOS, introduced: 17, obsoleted: 18)
|
||||||
|
public init<Element: Sendable, Sequence>(
|
||||||
|
sequence: sending Sequence,
|
||||||
|
@_inheritActorContext performing block: @escaping @Sendable (Element) async -> Void
|
||||||
|
) where Sequence: AsyncSequence, Element == Sequence.Element {
|
||||||
|
self.task = Task {
|
||||||
|
do {
|
||||||
|
for try await element in sequence {
|
||||||
|
await block(element)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
|
print("Error iterating over sequence: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
@available(iOS 18, *)
|
||||||
public extension AsyncSequence where Element: Sendable, Failure == Never {
|
public extension AsyncSequence where Element: Sendable, Failure == Never {
|
||||||
/// Observes the elements yielded by this sequence and executes the given closure with each element.
|
/// Observes the elements yielded by this sequence and executes the given closure with each element.
|
||||||
///
|
///
|
||||||
|
|
@ -33,3 +34,32 @@ public extension AsyncSequence where Element: Sendable, Failure == Never {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS, introduced: 17, obsoleted: 18)
|
||||||
|
public extension AsyncSequence where Self: Sendable, Element: Sendable {
|
||||||
|
/// Observes the elements yielded by this sequence and executes the given closure with each element.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - block: A closure that's executed with each yielded element.
|
||||||
|
func monitor(
|
||||||
|
_ block: @escaping @Sendable (Element) async -> Void
|
||||||
|
) -> AsyncMonitor {
|
||||||
|
AsyncMonitor(sequence: self, performing: block)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Observes the elements yielded by this sequence and executes the given closure with each element the weakly-captured
|
||||||
|
/// context object.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - context: The object to capture weakly for use within the closure.
|
||||||
|
/// - block: A closure that's executed with each yielded element, and the `context`.
|
||||||
|
func monitor<Context: AnyObject & Sendable>(
|
||||||
|
context: Context,
|
||||||
|
_ block: @escaping @Sendable (Context, Element) async -> Void
|
||||||
|
) -> AsyncMonitor {
|
||||||
|
AsyncMonitor(sequence: self) { [weak context] element in
|
||||||
|
guard let context else { return }
|
||||||
|
await block(context, element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,30 @@ public import Foundation
|
||||||
|
|
||||||
extension KeyPath: @unchecked @retroactive Sendable where Value: Sendable {}
|
extension KeyPath: @unchecked @retroactive Sendable where Value: Sendable {}
|
||||||
|
|
||||||
|
public extension NSObjectProtocol where Self: NSObject {
|
||||||
|
/// Returns an `AsyncSequence` of `Value`s for all changes to the given key path on this object.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - keyPath: The key path to observe on this object. The value must be `Sendable`.
|
||||||
|
/// - options: KVO options to use for observation. Defaults to an empty set.
|
||||||
|
func values<Value: Sendable>(
|
||||||
|
for keyPath: KeyPath<Self, Value>,
|
||||||
|
options: NSKeyValueObservingOptions = []
|
||||||
|
) -> AsyncStream<Value> {
|
||||||
|
let (stream, continuation) = AsyncStream<Value>.makeStream()
|
||||||
|
let token: NSKeyValueObservation? = self.observe(keyPath, options: options) { object, _ in
|
||||||
|
continuation.yield(object[keyPath: keyPath])
|
||||||
|
}
|
||||||
|
// A nice side-effect of this is that the stream retains the token automatically.
|
||||||
|
let locker = ValueLocker(value: token)
|
||||||
|
continuation.onTermination = { _ in
|
||||||
|
locker.modify { $0 = nil }
|
||||||
|
}
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 18, *)
|
||||||
public extension NSObjectProtocol where Self: NSObject {
|
public extension NSObjectProtocol where Self: NSObject {
|
||||||
/// Observes changes to the specified key path on the object and asynchronously yields each value. Values must be `Sendable`.
|
/// Observes changes to the specified key path on the object and asynchronously yields each value. Values must be `Sendable`.
|
||||||
///
|
///
|
||||||
|
|
@ -17,25 +41,22 @@ public extension NSObjectProtocol where Self: NSObject {
|
||||||
values(for: keyPath, options: options)
|
values(for: keyPath, options: options)
|
||||||
.monitor(changeHandler)
|
.monitor(changeHandler)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns an `AsyncSequence` of `Value`s for all changes to the given key path on this object.
|
@available(iOS, introduced: 17, obsoleted: 18)
|
||||||
|
public extension NSObjectProtocol where Self: NSObject {
|
||||||
|
/// Observes changes to the specified key path on the object and asynchronously yields each value. Values must be `Sendable`.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - keyPath: The key path to observe on this object. The value must be `Sendable`.
|
/// - keyPath: The key path to observe on this object. The value must be `Sendable`.
|
||||||
/// - options: KVO options to use for observation. Defaults to an empty set.
|
/// - options: KVO options to use for observation. Defaults to an empty set.
|
||||||
func values<Value: Sendable>(
|
/// - changeHandler: A closure that's executed with each new value.
|
||||||
|
func monitorValues<Value: Sendable>(
|
||||||
for keyPath: KeyPath<Self, Value>,
|
for keyPath: KeyPath<Self, Value>,
|
||||||
options: NSKeyValueObservingOptions = []
|
options: NSKeyValueObservingOptions = [],
|
||||||
) -> some AsyncSequence<Value, Never> {
|
changeHandler: @escaping @Sendable (Value) -> Void
|
||||||
let (stream, continuation) = AsyncStream<Value>.makeStream()
|
) -> any AsyncCancellable {
|
||||||
let token: NSKeyValueObservation? = self.observe(keyPath, options: options) { object, _ in
|
values(for: keyPath, options: options)
|
||||||
continuation.yield(object[keyPath: keyPath])
|
.monitor(changeHandler)
|
||||||
}
|
|
||||||
// A nice side-effect of this is that the stream retains the token automatically.
|
|
||||||
let locker = ValueLocker(value: token)
|
|
||||||
continuation.onTermination = { _ in
|
|
||||||
locker.modify { $0 = nil }
|
|
||||||
}
|
|
||||||
return stream
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class AsyncKVOTests {
|
||||||
#expect(values.count == total)
|
#expect(values.count == total)
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's important that the test or the progress-observing task are not on the same actor, so
|
// It's important that the test and the progress-observing task are not on the same actor, so
|
||||||
// we make the test @MainActor and observe progress values on another actor. Otherwise it's a
|
// we make the test @MainActor and observe progress values on another actor. Otherwise it's a
|
||||||
// deadlock.
|
// deadlock.
|
||||||
@Test(.timeLimit(.minutes(1)))
|
@Test(.timeLimit(.minutes(1)))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue