diff --git a/Sources/Flow/ActionHandler/ActionHandler.swift b/Sources/Flow/ActionHandler/ActionHandler.swift index 5305e3b..ce21128 100644 --- a/Sources/Flow/ActionHandler/ActionHandler.swift +++ b/Sources/Flow/ActionHandler/ActionHandler.swift @@ -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 = { 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 = + @MainActor (Action, State) async -> ActionTask + /// 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 @@ -195,8 +226,25 @@ public final class ActionHandler Void) -> ActionHandler< Action, State, ActionResult > { @@ -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) -> ActionTask @@ -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 > { diff --git a/Sources/Flow/ActionHandler/ActionProcessor.swift b/Sources/Flow/ActionHandler/ActionProcessor.swift index 645c840..ff4f757 100644 --- a/Sources/Flow/ActionHandler/ActionProcessor.swift +++ b/Sources/Flow/ActionHandler/ActionProcessor.swift @@ -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 = - @MainActor (Action, State) async -> - ActionTask - /// Error handler closure that can mutate state in response to errors. public typealias StateErrorHandler = (Error, State) -> Void diff --git a/Sources/Flow/Store/Feature.swift b/Sources/Flow/Store/Feature.swift index 12360f7..728c666 100644 --- a/Sources/Flow/Store/Feature.swift +++ b/Sources/Flow/Store/Feature.swift @@ -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 /// } @@ -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: /// @@ -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. @@ -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 {