From 40e4e4bc9c8dbdcf00b71ec0d69a886d5341f51c Mon Sep 17 00:00:00 2001 From: Andreas Grosam Date: Sat, 6 Jun 2026 12:16:25 +0200 Subject: [PATCH] feat: Add parameter env to observe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary • Pass Env to all observe handlers for consistent dependency access. • Add readable descriptions for EffectType/TransducerEffect to aid logging/tests. • Use task(id:) in README Search example (request-style intent). • Update README package version to 0.8.0. Changes • README: switch run → task in Search example; bump SPM version to 0.8.0. • Transducer.observe: handlers now take (input, value, env); isolated variants take (input, value, env, isolated); add Env: Sendable where needed. • Diagnostics: CustomStringConvertible for EffectType and TransducerEffect. • Tests: update observe handler signatures and variable names. Migration • Update observe closures: • Before: { input, value in ... } • After: { input, value, env in ... } • Prefer task(id:) over run(id:) when returning a result to requesters. --- .../EffectObservable/Transducer.observe.swift | 28 ++++----- .../Transducer/TransducerEffect.swift | 59 +++++++++++++++++++ Tests/EffectComponents/EffectViewTests.swift | 12 ++-- 3 files changed, 79 insertions(+), 20 deletions(-) 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") }