diff --git a/Sources/EffectComponents/EffectObservable/Transducer.observe.swift b/Sources/EffectComponents/EffectObservable/Transducer.observe.swift index 902b4e7..ef6feeb 100644 --- a/Sources/EffectComponents/EffectObservable/Transducer.observe.swift +++ b/Sources/EffectComponents/EffectObservable/Transducer.observe.swift @@ -43,19 +43,19 @@ extension Transducer where Effect == TransducerEffect { keyPath: KeyPath, id: TaskIdentifier? = "observe", priority: TaskPriority? = nil, - handler: @escaping @Sendable @isolated(any) (any TransducerInput & Sendable, Value) async -> Void + handler: @escaping @Sendable @isolated(any) (any TransducerInput & Sendable, Value, Env) async -> Void ) -> Effect - where Object: Observable & AnyObject & Sendable, Value: Sendable + where Object: Observable & AnyObject & Sendable, Value: Sendable, Env: Sendable { let box = SendableKeyPath(keyPath: keyPath) let envKeyPathBox = SendableKeyPath(keyPath: envKeyPath) return task(id: id, priority: priority) { input, env in do { let weakObject = WeakObject(object: env[keyPath: envKeyPathBox.keyPath]) - await handler(input, try observedValue(weakObject, keyPath: box)) + await handler(input, try observedValue(weakObject, keyPath: box), env) while true { try await _waitForObservationChange(weakObject, keyPath: box) - await handler(input, try observedValue(weakObject, keyPath: box)) + await handler(input, try observedValue(weakObject, keyPath: box), env) } } catch is CancellationError { // Transducer logic has cancelled. Do not rethrow. @@ -95,7 +95,7 @@ extension Transducer where Effect == TransducerEffect { keyPath: KeyPath, id: TaskIdentifier? = "observe", priority: TaskPriority? = nil, - isolatedHandler: @escaping (any TransducerInput, Value, isolated any Actor) async -> Void + isolatedHandler: @escaping (any TransducerInput, Value, Env, isolated any Actor) async -> Void ) -> Effect where Object: Observable & AnyObject & Sendable, Value: Sendable { @@ -108,10 +108,10 @@ extension Transducer where Effect == TransducerEffect { "observe(isolatedOperation:) requires a non-nil matching system actor. Actor hosts must provide isolation. Expected \(String(describing: systemActor)), got \(isolation)." ) let weakObject = WeakObject(object: env[keyPath: envKeyPathBox.keyPath]) - await isolatedHandler(input, try observedValue(weakObject, keyPath: box), isolation) + await isolatedHandler(input, try observedValue(weakObject, keyPath: box), env, isolation) while true { try await _waitForObservationChange(weakObject, keyPath: box) - await isolatedHandler(input, try observedValue(weakObject, keyPath: box), isolation) + await isolatedHandler(input, try observedValue(weakObject, keyPath: box), env, isolation) } } catch is CancellationError { @@ -164,18 +164,18 @@ extension Transducer where Effect == TransducerEffect { keyPath: KeyPath, id: TaskIdentifier? = "observe", priority: TaskPriority? = nil, - operation: @escaping @Sendable @isolated(any) (any TransducerInput & Sendable, Value) async -> Void + operation: @escaping @Sendable @isolated(any) (any TransducerInput & Sendable, Value, Env) async -> Void ) -> Effect - where Object: Observable & AnyObject & Sendable, Value: Sendable + where Object: Observable & AnyObject & Sendable, Value: Sendable, Env: Sendable { let box = SendableKeyPath(keyPath: keyPath) let weakObject = WeakObject(object: object) return task(id: id, priority: priority) { input, env in do { - await operation(input, try observedValue(weakObject, keyPath: box)) + await operation(input, try observedValue(weakObject, keyPath: box), env) while true { try await _waitForObservationChange(weakObject, keyPath: box) - await operation(input, try observedValue(weakObject, keyPath: box)) + await operation(input, try observedValue(weakObject, keyPath: box), env) } } catch is CancellationError { // Expected termination path for explicit cancel(name) or view teardown. @@ -207,7 +207,7 @@ extension Transducer where Effect == TransducerEffect { keyPath: KeyPath, id: TaskIdentifier? = "observe", priority: TaskPriority? = nil, - isolatedOperation: @escaping (any TransducerInput, Value, isolated any Actor) async -> Void + isolatedOperation: @escaping (any TransducerInput, Value, Env, isolated any Actor) async -> Void ) -> Effect where Object: Observable & AnyObject & Sendable, Value: Sendable { @@ -219,10 +219,10 @@ extension Transducer where Effect == TransducerEffect { systemActor != nil && systemActor === isolation, "observe(isolatedOperation:) requires a non-nil matching system actor. Actor hosts must provide isolation. Expected \(String(describing: systemActor)), got \(isolation)." ) - await isolatedOperation(input, try observedValue(weakObject, keyPath: box), isolation) + await isolatedOperation(input, try observedValue(weakObject, keyPath: box), env, isolation) while true { try await _waitForObservationChange(weakObject, keyPath: box) - await isolatedOperation(input, try observedValue(weakObject, keyPath: box), isolation) + await isolatedOperation(input, try observedValue(weakObject, keyPath: box), env, isolation) } } catch is CancellationError { // Expected termination path for explicit cancel(name) or view teardown. diff --git a/Sources/EffectComponents/Transducer/TransducerEffect.swift b/Sources/EffectComponents/Transducer/TransducerEffect.swift index 9c7fa21..25a3fe4 100644 --- a/Sources/EffectComponents/Transducer/TransducerEffect.swift +++ b/Sources/EffectComponents/Transducer/TransducerEffect.swift @@ -95,3 +95,62 @@ enum EffectType { case _sequence([TransducerEffect]) } +extension EffectType: CustomStringConvertible { + + public var description: String { + + switch self { + case ._task( + id: let id, + priority: let priority, + option: let option, + _ + ): + return "task(\(id, default: "_"), option: \(option))" + + case ._taskIsolated( + id: let id, + priority: let priority, + option: let option, + _ + ): + return "task(\(id, default: "_"), option: \(option))" + + + case ._taskNonsending( + id: let id, + priority: let priority, + option: let option, + _ + ): + return "task(\(id, default: "_"), option: \(option))" + + case ._actionSync: + return "action" + + case ._actionAsync: + return "action async" + + case ._actionAsyncIsolated: + return "action async" + + case ._event(let event): + return "event(\(event))" + + case ._cancel(let taskIdentifier): + return "cancel(\(taskIdentifier))" + + case ._sequence(let effects): + return "sequence(\(effects))" + + case .none: + return "none" + } + } +} + +extension TransducerEffect: CustomStringConvertible { + public var description: String { + return "\(type)" + } +} diff --git a/Tests/EffectComponents/EffectViewTests.swift b/Tests/EffectComponents/EffectViewTests.swift index c5b0133..26b87dd 100644 --- a/Tests/EffectComponents/EffectViewTests.swift +++ b/Tests/EffectComponents/EffectViewTests.swift @@ -491,17 +491,17 @@ struct EffectViewTests { enum Event: Sendable { case watch(ObservableCounter), tick(Int) } static func update(_ state: inout State, event: Event) -> TransducerEffect? { switch event { - case .watch(let c): - return observe(c, keyPath: \.value) { input, v in + case .watch(let observable): + return observe(observable, keyPath: \.value) { input, value, env in // Regarding: using observe with isolated action // Note: request is *nonisolated* for this Input. Thus we // cannot use `isolatedOperation` - we need to have // a Sendable operation which also requires Input to be // sendable! - try? await input.request(.tick(v)) + try? await input.request(.tick(value)) } - case .tick(let v): - state.latest = v + case .tick(let value): + state.latest = value return nil } } @@ -576,7 +576,7 @@ struct EffectViewTests { static func update(_ state: inout State, event: Event) -> Effect? { switch event { case .start: - return observe(\.counter, keyPath: \.value, id: "observe") { input, value in + return observe(\.counter, keyPath: \.value, id: "observe") { input, value, env in try? await input.request(.tick(value)) print("request(.tick(\(value))) finished") }