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
20 changes: 14 additions & 6 deletions Sources/Flow/Store/ActionTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ public struct ActionTask<Action, State, ActionResult: Sendable> {
/// Execute an asynchronous operation that returns a result
case run(
id: String,
name: String?,
operation: @MainActor (State) async throws -> ActionResult,
onError: (@MainActor (Error, State) -> Void)?,
cancelInFlight: Bool,
Expand Down Expand Up @@ -285,24 +286,28 @@ extension ActionTask {
/// return .created(id: outcome.id)
/// }
///
/// // Make it cancellable with ID
/// return .run { state in
/// // Named task for better debugging
/// return .run(name: "πŸ”„ Fetch user data") { state in
/// let data = try await fetch()
/// state.data = data
/// return .fetched
/// }
/// .cancellable(id: "fetch", cancelInFlight: true)
/// ```
///
/// - Parameter operation: The async operation to execute, receiving mutable state and returning a result
/// - Parameters:
/// - name: Optional human-readable name for the task (useful for debugging and profiling)
/// - operation: The async operation to execute, receiving mutable state and returning a result
/// - Returns: A new `ActionTask` that will execute the operation
public static func run(
name: String? = nil,
operation: @escaping @MainActor (State) async throws -> ActionResult
) -> ActionTask {
let taskId = TaskIdGenerator.generate()
return ActionTask(
operation: .run(
id: taskId,
name: name,
operation: operation,
onError: nil,
cancelInFlight: false,
Expand Down Expand Up @@ -542,10 +547,11 @@ extension ActionTask {
/// - Returns: A new `ActionTask` with the error handler attached
public func `catch`(_ handler: @escaping @MainActor (Error, State) -> Void) -> ActionTask {
switch operation {
case .run(let id, let op, _, let cancelInFlight, let priority):
case .run(let id, let name, let op, _, let cancelInFlight, let priority):
return ActionTask(
operation: .run(
id: id,
name: name,
operation: op,
onError: handler,
cancelInFlight: cancelInFlight,
Expand Down Expand Up @@ -590,11 +596,12 @@ extension ActionTask {
cancelInFlight: Bool = false
) -> ActionTask {
switch operation {
case .run(_, let op, let onError, _, let priority):
case .run(_, let name, let op, let onError, _, let priority):
let stringId = id.taskIdString
return ActionTask(
operation: .run(
id: stringId,
name: name,
operation: op,
onError: onError,
cancelInFlight: cancelInFlight,
Expand Down Expand Up @@ -641,10 +648,11 @@ extension ActionTask {
/// - Returns: A new `ActionTask` with the specified priority
public func priority(_ priority: TaskPriority) -> ActionTask {
switch operation {
case .run(let id, let op, let onError, let cancelInFlight, _):
case .run(let id, let name, let op, let onError, let cancelInFlight, _):
return ActionTask(
operation: .run(
id: id,
name: name,
operation: op,
onError: onError,
cancelInFlight: cancelInFlight,
Expand Down
6 changes: 5 additions & 1 deletion Sources/Flow/Store/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,10 @@ public final class Store<F: Feature> {
///
/// This helper method handles the complexity of executing async operations,
/// managing task cancellation, and error handling.
// swiftlint:disable:next function_parameter_count
private func executeRunTask(
id: String,
name: String?,
operation: @escaping @MainActor (F.State) async throws -> F.ActionResult,
onError: (@MainActor (Error, F.State) -> Void)?,
cancelInFlight: Bool,
Expand All @@ -220,6 +222,7 @@ public final class Store<F: Feature> {

let runningTask = taskManager.executeTask(
id: id,
name: name,
operation: { @MainActor [weak self] in
guard let self else {
throw StoreError.deallocated
Expand Down Expand Up @@ -310,9 +313,10 @@ public final class Store<F: Feature> {
case .just(let result):
return result

case .run(let id, let operation, let onError, let cancelInFlight, let priority):
case .run(let id, let name, let operation, let onError, let cancelInFlight, let priority):
return try await executeRunTask(
id: id,
name: name,
operation: operation,
onError: onError,
cancelInFlight: cancelInFlight,
Expand Down
5 changes: 4 additions & 1 deletion Sources/Flow/Store/TaskManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ public final class TaskManager {
///
/// - Parameters:
/// - id: Unique identifier for the task (string representation)
/// - name: Optional human-readable name for the task (useful for debugging and profiling)
/// - operation: The asynchronous operation to execute
/// - onError: Optional error handler called if the operation throws
/// - priority: Optional task priority (defaults to nil, using system default)
Expand All @@ -123,6 +124,7 @@ public final class TaskManager {
/// ```swift
/// let task = taskManager.executeTask(
/// id: "loadProfile",
/// name: "πŸ”„ Load user profile",
/// operation: {
/// let profile = try await api.fetchProfile()
/// await store.send(.profileLoaded(profile))
Expand All @@ -140,6 +142,7 @@ public final class TaskManager {
@discardableResult
public func executeTask(
id: String,
name: String? = nil,
operation: @escaping () async throws -> Void,
onError: ((Error) async -> Void)?,
priority: TaskPriority? = nil
Expand All @@ -152,7 +155,7 @@ public final class TaskManager {

// Use [weak self] to prevent retain cycle (TaskManager ← runningTasks ← Task)
// Ensures deinit runs when Store deallocates, cancelling all tasks
let task = Task(priority: priority) { @MainActor [weak self] in
let task = Task(name: name, priority: priority) { @MainActor [weak self] in
guard let self else { return }

// Defer ensures cleanup happens exactly once, regardless of how the task completes
Expand Down
8 changes: 4 additions & 4 deletions Tests/UnitTests/ActionHandler/ActionHandlerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ import Testing

// THEN: Should return run task
#expect(state.isLoading)
if case .run(let id, _, _, _, _) = task.operation {
if case .run(let id, _, _, _, _, _) = task.operation {
#expect(id == "test")
} else {
Issue.record("Expected run task")
Expand Down Expand Up @@ -286,7 +286,7 @@ import Testing

// THEN: Task should be transformed
#expect(state.count == 1)
if case .run(let id, _, _, _, _) = task.operation {
if case .run(let id, _, _, _, _, _) = task.operation {
#expect(id == "transformed")
} else {
Issue.record("Expected run task")
Expand All @@ -301,7 +301,7 @@ import Testing
}
.transform { task in
switch task.operation {
case .run(let id, _, _, _, _):
case .run(let id, _, _, _, _, _):
return .cancel(id: id)
default:
return task
Expand Down Expand Up @@ -466,7 +466,7 @@ import Testing

let task = await sut.handle(action: .asyncOp, state: state)
#expect(state.isLoading)
if case .run(let id, _, _, _, _) = task.operation {
if case .run(let id, _, _, _, _, _) = task.operation {
#expect(id == "complex")
}

Expand Down
8 changes: 4 additions & 4 deletions Tests/UnitTests/ActionHandler/ActionProcessorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ import Testing

// THEN: Should return run task
#expect(state.isLoading)
if case .run(let id, _, _, _, _) = task.operation {
if case .run(let id, _, _, _, _, _) = task.operation {
#expect(id == "test-task")
} else {
Issue.record("Expected run task")
Expand Down Expand Up @@ -282,7 +282,7 @@ import Testing

// THEN: Task should be transformed
#expect(state.count == 1)
if case .run(let id, _, _, _, _) = task.operation {
if case .run(let id, _, _, _, _, _) = task.operation {
#expect(id == "transformed")
} else {
Issue.record("Expected run task")
Expand All @@ -297,7 +297,7 @@ import Testing
}
.transform { task in
switch task.operation {
case .run(let id, _, _, _, _):
case .run(let id, _, _, _, _, _):
return .cancel(id: id)
default:
return task
Expand Down Expand Up @@ -398,7 +398,7 @@ import Testing
#expect(state.count == 15)
#expect(middlewareExecuted)
#expect(state.errorMessage == nil)
if case .run(let id, _, _, _, _) = task.operation {
if case .run(let id, _, _, _, _, _) = task.operation {
#expect(id == "main-task")
}
}
Expand Down
20 changes: 10 additions & 10 deletions Tests/UnitTests/Store/ActionTaskCancellableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import Testing

// THEN: Should have the specified ID and cancelInFlight = false
switch sut.operation {
case .run(let id, _, _, let cancelInFlight, _):
case .run(let id, _, _, _, let cancelInFlight, _):
#expect(id == "search")
#expect(!cancelInFlight)
default:
Expand All @@ -47,7 +47,7 @@ import Testing

// THEN: Should have cancelInFlight = true
switch sut.operation {
case .run(let id, _, _, let cancelInFlight, _):
case .run(let id, _, _, _, let cancelInFlight, _):
#expect(id == "search")
#expect(cancelInFlight)
default:
Expand All @@ -62,7 +62,7 @@ import Testing

// THEN: Should use the new ID from cancellable
switch sut.operation {
case .run(let id, _, _, _, _):
case .run(let id, _, _, _, _, _):
#expect(id == "new-id")
default:
Issue.record("Expected run task")
Expand Down Expand Up @@ -105,7 +105,7 @@ import Testing

// THEN: Should have both ID, cancelInFlight, and error handler
switch sut.operation {
case .run(let id, _, let onError, let cancelInFlight, _):
case .run(let id, _, _, let onError, let cancelInFlight, _):
#expect(id == "search")
#expect(cancelInFlight)
#expect(onError != nil)
Expand All @@ -122,7 +122,7 @@ import Testing

// THEN: Should preserve error handler and set cancellable
switch sut.operation {
case .run(let id, _, let onError, let cancelInFlight, _):
case .run(let id, _, _, let onError, let cancelInFlight, _):
#expect(id == "search")
#expect(cancelInFlight)
#expect(onError != nil)
Expand All @@ -138,7 +138,7 @@ import Testing

// THEN: Should convert Int to String
switch sut.operation {
case .run(let id, _, _, let cancelInFlight, _):
case .run(let id, _, _, _, let cancelInFlight, _):
#expect(id == "42")
#expect(cancelInFlight)
default:
Expand All @@ -154,7 +154,7 @@ import Testing

// THEN: Should convert UUID to String
switch sut.operation {
case .run(let id, _, _, let cancelInFlight, _):
case .run(let id, _, _, _, let cancelInFlight, _):
#expect(id == uuid.uuidString)
#expect(cancelInFlight)
default:
Expand All @@ -174,7 +174,7 @@ import Testing

// THEN: Should use enum raw value
switch sut.operation {
case .run(let id, _, _, let cancelInFlight, _):
case .run(let id, _, _, _, let cancelInFlight, _):
#expect(id == "search")
#expect(cancelInFlight)
default:
Expand All @@ -189,7 +189,7 @@ import Testing

// THEN: cancelInFlight should default to false
switch sut.operation {
case .run(_, _, _, let cancelInFlight, _):
case .run(_, _, _, _, let cancelInFlight, _):
#expect(!cancelInFlight)
default:
Issue.record("Expected run task")
Expand All @@ -204,7 +204,7 @@ import Testing

// THEN: Last call should override
switch sut.operation {
case .run(let id, _, _, let cancelInFlight, _):
case .run(let id, _, _, _, let cancelInFlight, _):
#expect(id == "second")
#expect(cancelInFlight)
default:
Expand Down
20 changes: 10 additions & 10 deletions Tests/UnitTests/Store/ActionTaskPriorityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import Testing

// THEN: Should have high priority
switch result.operation {
case .run(_, _, _, _, let priority):
case .run(_, _, _, _, _, let priority):
#expect(priority == .high)
default:
Issue.record("Expected run task")
Expand All @@ -51,7 +51,7 @@ import Testing

// THEN: Should have low priority
switch result.operation {
case .run(_, _, _, _, let priority):
case .run(_, _, _, _, _, let priority):
#expect(priority == .low)
default:
Issue.record("Expected run task")
Expand All @@ -69,7 +69,7 @@ import Testing

// THEN: Should have background priority
switch result.operation {
case .run(_, _, _, _, let priority):
case .run(_, _, _, _, _, let priority):
#expect(priority == .background)
default:
Issue.record("Expected run task")
Expand All @@ -87,7 +87,7 @@ import Testing

// THEN: Should have userInitiated priority
switch result.operation {
case .run(_, _, _, _, let priority):
case .run(_, _, _, _, _, let priority):
#expect(priority == .userInitiated)
default:
Issue.record("Expected run task")
Expand All @@ -102,7 +102,7 @@ import Testing

// THEN: Priority should be nil (system default)
switch sut.operation {
case .run(_, _, _, _, let priority):
case .run(_, _, _, _, _, let priority):
#expect(priority == nil)
default:
Issue.record("Expected run task")
Expand All @@ -125,7 +125,7 @@ import Testing

// THEN: Should have both priority and cancellable ID
switch result.operation {
case .run(let id, _, _, let cancelInFlight, let priority):
case .run(let id, _, _, _, let cancelInFlight, let priority):
#expect(id == "test-task")
#expect(cancelInFlight == true)
#expect(priority == .high)
Expand All @@ -150,7 +150,7 @@ import Testing

// THEN: Should have both priority and error handler
switch result.operation {
case .run(_, _, let onError, _, let priority):
case .run(_, _, _, let onError, _, let priority):
#expect(priority == .userInitiated)
#expect(onError != nil)
default:
Expand All @@ -175,7 +175,7 @@ import Testing

// THEN: Should preserve all configurations
switch result.operation {
case .run(let id, _, let onError, let cancelInFlight, let priority):
case .run(let id, _, _, let onError, let cancelInFlight, let priority):
#expect(id == "full-chain")
#expect(cancelInFlight == false)
#expect(priority == .high)
Expand All @@ -202,7 +202,7 @@ import Testing

// THEN: Should preserve all configurations regardless of order
switch result.operation {
case .run(let id, _, let onError, _, let priority):
case .run(let id, _, _, let onError, _, let priority):
#expect(id == "order-test")
#expect(priority == .low)
#expect(onError != nil)
Expand All @@ -225,7 +225,7 @@ import Testing

// THEN: Should have the new priority
switch result.operation {
case .run(_, _, _, _, let priority):
case .run(_, _, _, _, _, let priority):
#expect(priority == .low)
default:
Issue.record("Expected run task")
Expand Down
Loading
Loading