Skip to content
Merged
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
92 changes: 92 additions & 0 deletions Sources/Flow/ActionHandler/ActionHandler.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
import Foundation

/// Action execution closure that mutates state and returns a task.
///
/// This typealias defines the signature for action processing logic used in ``ActionHandler``.
/// The closure receives an action and state, performs any necessary state mutations,
/// and returns an ``ActionTask`` for asynchronous side effects.
///
/// All execution occurs on the **MainActor**, ensuring thread-safe state mutations.
///
/// ## Example
/// ```swift
/// let execution: ActionExecution<MyAction, MyState, Void> = { action, state in
/// switch action {
/// case .increment:
/// state.count += 1
/// return .none
/// }
/// }
/// ```
///
/// ## Type Parameters
/// - `Action`: The action type to process (must be Sendable)
/// - `State`: The state type to mutate (must be AnyObject/reference type)
/// - `ActionResult`: The result type returned from action processing (must be Sendable)
///
/// ## See Also
/// - ``ActionHandler/init(_:)``
///
/// - Note: Type constraints match Feature protocol requirements to ensure consistency.
public typealias ActionExecution<Action: Sendable, State: AnyObject, ActionResult: Sendable> =
@MainActor (Action, State) async -> ActionTask<Action, State, ActionResult>

/// A facade for action processing with fluent method chaining capabilities that can return typed results.
///
/// `ActionHandler` provides a clean, composable API for defining how your feature
Expand Down Expand Up @@ -195,8 +226,25 @@ public final class ActionHandler<Action: Sendable, State: AnyObject, ActionResul
extension ActionHandler {
/// Adds error handling to the action processing pipeline.
///
/// The error handler receives any errors thrown during action processing
/// and can update state accordingly (e.g., setting error messages, resetting loading flags).
///
/// - Parameter errorHandler: A closure that handles errors
/// - Returns: A new ActionHandler with error handling
///
/// - Note: If you call `onError` multiple times, only the **last** handler will be used.
/// Each call replaces the previous error handler.
///
/// ## Example
/// ```swift
/// ActionHandler { action, state in
/// // action processing
/// }
/// .onError { error, state in
/// state.errorMessage = error.localizedDescription
/// state.isLoading = false
/// }
/// ```
public func onError(_ errorHandler: @escaping (Error, State) -> Void) -> ActionHandler<
Action, State, ActionResult
> {
Expand All @@ -205,8 +253,34 @@ extension ActionHandler {

/// Transforms the task returned by action processing.
///
/// Use this to add cross-cutting concerns like logging, analytics, or monitoring
/// to all tasks without modifying individual action handlers.
///
/// - Parameter taskTransform: A closure that transforms the task
/// - Returns: A new ActionHandler with task transformation
///
/// ## Example: Add Logging to All Tasks
/// ```swift
/// ActionHandler { action, state in
/// // action processing
/// }
/// .transform { task in
/// switch task.operation {
/// case .run(let id, let name, let operation, let onError, let cancelInFlight, let priority):
/// return .run(id: id, name: name, priority: priority) { state in
/// print("Task '\(name ?? id)' starting")
/// let result = try await operation(state)
/// print("Task '\(name ?? id)' completed")
/// return result
/// } onError: { error, state in
/// print("Task '\(name ?? id)' failed: \(error)")
/// onError?(error, state)
/// }
/// default:
/// return task
/// }
/// }
/// ```
public func transform(
_ taskTransform: @escaping (ActionTask<Action, State, ActionResult>)
-> ActionTask<Action, State, ActionResult>
Expand All @@ -216,8 +290,26 @@ extension ActionHandler {

/// Adds custom middleware to the action processing pipeline.
///
/// Middleware is executed in the order it's added. Call `use` multiple times
/// to add multiple middlewares, and they will execute sequentially.
///
/// - Parameter middleware: The middleware to add
/// - Returns: A new ActionHandler with the middleware added
///
/// ## Example: Add Multiple Middlewares
/// ```swift
/// ActionHandler { action, state in
/// // action processing
/// }
/// .use(LoggingMiddleware()) // Executes first
/// .use(AnalyticsMiddleware()) // Executes second
/// .use(TimingMiddleware()) // Executes third
/// ```
///
/// - Note: Middleware executes in **registration order**:
/// - `beforeAction` hooks run in order (first → last)
/// - Action logic executes
/// - `afterAction` hooks run in order (first → last)
public func use(_ middleware: some BaseActionMiddleware) -> ActionHandler<
Action, State, ActionResult
> {
Expand Down
7 changes: 0 additions & 7 deletions Sources/Flow/ActionHandler/ActionProcessor.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import Foundation

/// Action execution closure that mutates state and returns a task.
///
/// - Note: Type constraints match Feature protocol requirements to ensure consistency.
public typealias ActionExecution<Action: Sendable, State: AnyObject, ActionResult: Sendable> =
@MainActor (Action, State) async ->
ActionTask<Action, State, ActionResult>

/// Error handler closure that can mutate state in response to errors.
public typealias StateErrorHandler<State> = (Error, State) -> Void

Expand Down
18 changes: 16 additions & 2 deletions Sources/Flow/Store/Feature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ import Foundation
/// switch action {
/// case .login(let credentials):
/// state.isLoading = true // ← Direct state mutation
/// return .run { state in // ← Async task
/// return .run { state in // ← Same state instance (reference type)
/// let user = try await authService.login(credentials)
/// state.user = user
/// state.user = user // ← Mutations visible to outer scope
/// state.isAuthenticated = true
/// state.isLoading = false
/// }
Expand All @@ -69,6 +69,11 @@ import Foundation
/// }
/// ```
///
/// - Note: In the `.run` closure, the `state` parameter refers to the same instance as the outer
/// `state` parameter (State is a reference type). All mutations inside `.run` are immediately
/// visible to the outer scope. This allows you to update state both before and during async
/// operations while maintaining a single source of truth.
///
/// ## Task Management
/// Your action handlers can return different task types:
///
Expand Down Expand Up @@ -138,6 +143,10 @@ public protocol Feature: Sendable {
/// ```
///
/// - Note: @Observable requires class types for SwiftUI observation
/// - Warning: Your State class **must** use the `@Observable` macro for SwiftUI integration.
/// The type system cannot enforce this requirement. Forgetting `@Observable` will cause
/// SwiftUI views to not update automatically when state changes, and the compiler will
/// not warn you. Always verify your State class has the `@Observable` annotation.
associatedtype State: AnyObject

/// The type representing the result returned from action processing.
Expand Down Expand Up @@ -223,6 +232,11 @@ public protocol Feature: Sendable {
/// ensuring thread-safe UI updates. It returns a ``ActionTask`` to handle
/// any asynchronous side effects.
///
/// - Note: The `handle()` method is called **once** during Store initialization.
/// The returned ``ActionHandler`` instance is reused for all subsequent actions.
/// Do not call `handle()` multiple times or store it separately - let the Store
/// manage the ActionHandler lifecycle.
///
/// ## Example
/// ```swift
/// func handle() -> ActionHandler<Action, State, ActionResult> {
Expand Down
Loading