diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/EffectView.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/EffectComponents.xcscheme
similarity index 83%
rename from .swiftpm/xcode/xcshareddata/xcschemes/EffectView.xcscheme
rename to .swiftpm/xcode/xcshareddata/xcschemes/EffectComponents.xcscheme
index bda2159..3476502 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/EffectView.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/EffectComponents.xcscheme
@@ -15,9 +15,9 @@
buildForAnalyzing = "YES">
@@ -34,9 +34,9 @@
skipped = "NO">
@@ -63,9 +63,9 @@
diff --git a/Sources/EffectComponents/Transducer/Transducer.Effects.swift b/Sources/EffectComponents/Transducer/Transducer.Effects.swift
index 32ad978..61f27a3 100644
--- a/Sources/EffectComponents/Transducer/Transducer.Effects.swift
+++ b/Sources/EffectComponents/Transducer/Transducer.Effects.swift
@@ -167,6 +167,17 @@ extension Transducer where Effect == TransducerEffect {
.init(._actionAsync(action))
}
+ /// Return an effect which when invoked executes an async step on a user specified global actor.
+ ///
+ /// The `action` closure receives `Env`. It runs synchronously on the system actor before any
+ /// other work proceeds.
+ ///
+ /// Async actions still do not create managed task identities of their own. Any
+ /// overlap semantics apply only once the chain reaches a named terminal task.
+ ///
+ /// - Parameter action: A synchronous closure receiving `Env`.
+ ///
+ /// - Returns: An effect.
@inline(__always)
public static func action(
_ action: sending @escaping @isolated(any) (Env) async -> Void
@@ -203,6 +214,17 @@ extension Transducer where Effect == TransducerEffect {
.init(._actionAsyncIsolated(action))
}
+ /// Return an effect which when invoked executes an async step on the system actor.
+ ///
+ /// The `action` closure receives `Env`. It runs synchronously on the system actor before any
+ /// other work proceeds.
+ ///
+ /// Async isolated actions still do not create managed task identities of their own.
+ /// Any overlap semantics apply only once the chain reaches a named terminal task.
+ ///
+ /// - Parameter action: A synchronous closure receiving `Env`.
+ ///
+ /// - Returns: An effect.
@inline(__always)
public static func action(
_ action: sending @escaping (Env, isolated any Actor) async -> Void
@@ -214,14 +236,37 @@ extension Transducer where Effect == TransducerEffect {
}
+ /// Returns an effect which, when invoked, feeds an event back into `update` immediately
+ /// in the current synchronous turn.
+ ///
+ /// Prefer this helper over the deprecated `event(_:)` to avoid naming conflicts with the
+ /// `update(_ state:event:)` parameter named `event`.
+ ///
+ /// - Parameter event: The next event to feed directly back into `update`.
+ /// - Returns: An effect.
+ @inline(__always)
+ public static func send(_ event: Event) -> Effect {
+ .init(._event(event))
+ }
+
/// Returns an effect which when invoked feeds `event` back into `update` immediately, in
/// the current synchronous turn.
///
+ /// ```swift
+ /// // Before: ambiguous inside `update` due to parameter
+ /// // named `event`
+ /// // return Self.event(.tick)
+ ///
+ /// // After: preferred helper
+ /// return send(.tick)
+ /// ```
+ ///
/// - Parameter event: The next event to feed directly back into `update`.
/// - Returns: An effect.
+ @available(*, deprecated, message: "Use send(_:) instead to avoid shadowing with the `update` parameter named `event`.")
@inline(__always)
public static func event(_ event: Event) -> Effect {
- .init(._event(event))
+ send(event)
}
/// Returns an effect which cancels the running task with the given identifier, if any.
diff --git a/Sources/EffectComponents/Transducer/Transducer.swift b/Sources/EffectComponents/Transducer/Transducer.swift
index f81f128..382e7a5 100644
--- a/Sources/EffectComponents/Transducer/Transducer.swift
+++ b/Sources/EffectComponents/Transducer/Transducer.swift
@@ -11,6 +11,110 @@ import Foundation
/// `update` mutates state synchronously and may return an effect describing
/// follow-up work. That work can emit more events later, but direct state
/// mutation still flows back through `update`.
+///
+///
+/// Example
+/// -------
+/// A minimal counter feature showing how to declare `State`, an `Event` enum (note the enum),
+/// the `update` reducer, and an `output` that returns a value from state.
+///
+/// ```swift
+/// enum CounterTransducer: Transducer {
+/// // Feature state owned by the runtime
+/// struct State {
+/// var count: Int = 0
+/// }
+///
+/// // Domain events that drive state transitions
+/// // (note: this is an enum)
+/// enum Event {
+/// case increment
+/// case decrement
+/// case reset
+/// case requestCurrentValue // e.g., a request-style event
+/// }
+///
+/// // No external dependencies for this simple example
+/// typealias Env = Void
+///
+/// // The effect returned by `update` defaults to
+/// // `TransducerEffect`
+/// // We keep `Output` as `Int` to return the current
+/// // counter value from `output`.
+/// typealias Output = Int
+///
+/// // Synchronous state mutation and next-effect
+/// // selection
+/// static func update(
+/// _ state: inout State, event: Event
+/// ) -> Effect? {
+/// switch event {
+/// case .increment:
+/// state.count += 1
+/// return nil
+///
+/// case .decrement:
+/// state.count -= 1
+/// return nil
+///
+/// case .reset:
+/// state.count = 0
+/// return nil
+///
+/// case .requestCurrentValue:
+/// // In a request flow, you can choose to end the
+/// // chain here with no further effect.
+/// // The runtime will call `output(state:event:)`
+/// // to produce the result.
+/// return nil
+/// }
+/// }
+///
+/// // Produces the terminal result for request-style
+/// // chains
+/// static func output(
+/// state: State, event: Event
+/// ) -> Output {
+/// // For this simple example, always return
+/// // the current count
+/// state.count
+/// }
+/// }
+/// ```
+///
+/// Requesting the current value
+/// ----------------------------
+/// If your runtime provides an `Input` that conforms to `TransducerInput`, you can
+/// request the current value by sending a request-style event and awaiting the output:
+///
+/// ```swift
+/// // Pseudocode demonstrating usage inside
+/// // a host runtime
+/// var state = CounterTransducer.State()
+/// let taskManager = TaskManager()
+/// let input: (any TransducerInput)? = /* provided by host */
+/// let env: CounterTransducer.Env = ()
+///
+/// // Fire some events that mutate state
+/// try await CounterTransducer.compute(
+/// event: .increment,
+/// continuation: nil,
+/// state: &state,
+/// taskManager: taskManager,
+/// input: input,
+/// env: env
+/// )
+///
+/// // Later, request the current value. Depending on
+/// // your host, this might be:
+/// if let input {
+/// // Resume with the value produced by
+/// // `output(state:event:)`
+/// let value: Int? = await input.request(.requestCurrentValue)
+/// // `value` is the current `state.count` at the
+/// // time the request settles
+/// }
+/// ```
public protocol Transducer: SendableMetatype {
/// Mutable feature state owned by the host runtime.
associatedtype State
diff --git a/Sources/EffectComponents/Transducer/TransducerInput.swift b/Sources/EffectComponents/Transducer/TransducerInput.swift
index 4a7f031..447c8f7 100644
--- a/Sources/EffectComponents/Transducer/TransducerInput.swift
+++ b/Sources/EffectComponents/Transducer/TransducerInput.swift
@@ -1,9 +1,10 @@
/// A handle for feeding events back into a transducer runtime.
///
-/// `TransducerInput` has two dispatch styles:
+/// `TransducerInput` has three dispatch styles:
///
/// - ``post(_:)`` sends an event without awaiting a result.
+/// - ``send(_:)`` suspends until the immediate reduction path has been processed.
/// - ``request(_:)`` suspends until the triggered effect chain settles and returns
/// the terminal `Output?` value, if any.
///
@@ -21,6 +22,29 @@
public protocol TransducerInput: SendableMetatype {
associatedtype Event
associatedtype Output
+
+ /// Sends `event` and suspends until the runtime has processed its immediate
+ /// reduction path.
+ ///
+ /// `send` is a deliberate backpressure boundary. It returns only after the
+ /// runtime has run `update` for `event` and any immediately returned
+ /// event/action chain, or after that chain has reached a terminal managed
+ /// task. If a task effect is started, `send` returns after the task has been
+ /// accepted by the runtime; it does not wait for the task operation to
+ /// complete and it does not return `Output`.
+ ///
+ /// Custom queued hosts must preserve this semantic. If events are transported
+ /// through a channel or mailbox, `send` must be acknowledged after the
+ /// consumer has processed the immediate reduction path, not merely after the
+ /// event has been enqueued. A queued implementation typically transports an
+ /// envelope carrying either no continuation (`post`), an acknowledgement
+ /// continuation (`send`), or an output continuation (`request`).
+ ///
+ /// - Parameter event: The event to send into the runtime.
+ /// - Throws: A runtime entry failure if the event cannot be accepted, or a
+ /// later cancellation or runtime failure before the immediate reduction path
+ /// has been acknowledged.
+ func send(_ event: sending Event) async throws
/// Schedules `event` without awaiting the resulting effect chain.
///
diff --git a/Tests/EffectComponents/EffectViewTests.swift b/Tests/EffectComponents/EffectViewTests.swift
index 59a0bc2..c5b0133 100644
--- a/Tests/EffectComponents/EffectViewTests.swift
+++ b/Tests/EffectComponents/EffectViewTests.swift
@@ -662,6 +662,12 @@ private final class EventSpy: @unchecked Sendable {
private struct TaskInput: TransducerInput, Sendable {
let onEvent: @Sendable @MainActor (Event) -> Void
+ func send(_ event: sending Event) async throws {
+ await MainActor.run {
+ onEvent(event)
+ }
+ }
+
func post(_ event: sending Event) {
Task { @MainActor in
onEvent(event)
diff --git a/Tests/EffectComponents/RunFailureLifecycleTests.swift b/Tests/EffectComponents/RunFailureLifecycleTests.swift
index b43a36e..bc735ab 100644
--- a/Tests/EffectComponents/RunFailureLifecycleTests.swift
+++ b/Tests/EffectComponents/RunFailureLifecycleTests.swift
@@ -15,6 +15,8 @@ struct RunFailureLifecycleTests {
}
private struct StubInput: TransducerInput, Sendable {
+ func send(_ event: sending Event) async throws {}
+
func post(_ event: sending Event) throws {}
func request(_ event: Event) async throws -> Output? {