MonoTask

public class MonoTask<TaskResult>

MonoTask: Single-Instance Task Executor with TTL Caching and Retry Logic

MonoTask is a thread-safe, high-performance task executor that ensures only one instance of a task runs at a time while providing intelligent result caching and retry capabilities.

Key Features:

  • Execution Merging: Multiple concurrent requests are merged into a single execution
  • TTL-based Caching: Results are cached for a configurable duration to avoid redundant work
  • Retry Logic: Automatic retry with exponential backoff for failed executions
  • Thread Safety: Full thread safety with fine-grained locking using semaphores
  • Queue Management: Separate queues for task execution and callback invocation
  • Manual Cache Control: Manual cache invalidation with execution strategy options

Architecture:

┌─────────────────┐    ┌──────────────┐    ┌─────────────────┐
   Execution            Caching           Callbacks     
   Merging       │───▶│   Layer      │───▶│   Distribution  
                                                        
  Single run          TTL cache        All waiters   
  Merge calls         Expiration       Same result   
└─────────────────┘    └──────────────┘    └─────────────────┘

Usage Examples:

// Basic usage with caching
let task = MonoTask<String>(resultExpireDuration: 60.0) { callback in
    // Expensive network call
    URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            callback(.failure(error))
        } else {
            callback(.success(String(data: data!, encoding: .utf8)!))
        }
    }.resume()
}

// Multiple concurrent calls - only one network request
let result1 = await task.asyncExecute() // Network call happens
let result2 = await task.asyncExecute() // Returns cached result
let result3 = await task.asyncExecute() // Returns cached result

// With retry logic
let retryTask = MonoTask<Data>(
    retry: .count(count: 3, intervalProxy: .exponentialBackoff(initialTimeInterval: 1.0, scaleRate: 2.0)),
    resultExpireDuration: 300.0
) { callback in
    performNetworkRequest(callback)
}

// Manual cache control
task.clearResult(ongoingExecutionStrategy: .cancel) // Cancel and clear

Thread Safety:

MonoTask uses a single internal semaphore for fine-grained thread safety:

  • Protects cached result, expiration timestamp, callback registration, and execution state

Performance:

  • Execution Merging: Prevents duplicate work when multiple callers request same task
  • TTL Caching: Avoids repeated expensive operations within cache period
  • Queue Separation: Task execution and callback invocation can run on different queues
  • Memory Efficient: Minimal overhead per task instance

Note

This class is designed for expensive, idempotent operations like network requests, database queries, or complex computations that benefit from caching and deduplication.

Type Aliases

  • Callback signature for receiving task execution results

    Declaration

    Swift

    typealias ResultCallback = (Result<TaskResult, Error>) -> Void

    Parameters

    result

    Success with TaskResult data or failure with Error

  • Callback-based execution block signature for traditional async patterns

    Declaration

    Swift

    typealias CallbackExecution = (@escaping ResultCallback) -> Void

    Parameters

    callback

    Callback to invoke with result when task completes

  • AsyncExecution Asynchronous

    Swift async/await execution block signature for modern async patterns

    Declaration

    Swift

    typealias AsyncExecution = () async -> Result<TaskResult, Error>

    Return Value

    Result with success data or failure error

Convenience Initializers

  • Create MonoTask with callback-based execution (traditional async pattern)

    Perfect for wrapping existing callback-based APIs like URLSession, Core Data, etc.

    Example:

    let networkTask = MonoTask<Data>(
        retry: .count(count: 3, intervalProxy: .exponentialBackoff(initialTimeInterval: 1.0)),
        resultExpireDuration: 300.0 // 5 minutes
    ) { callback in
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                callback(.failure(error))
            } else if let data = data {
                callback(.success(data))
            }
        }.resume()
    }
    

    Declaration

    Swift

    convenience init(
        retry: RetryCount = .never,
        resultExpireDuration: TimeInterval,
        taskQueue: DispatchQueue = DispatchQueue.global(),
        callbackQueue: DispatchQueue = DispatchQueue.global(),
        task: @escaping CallbackExecution
    )

    Parameters

    retry

    Retry configuration (default: no retries)

    resultExpireDuration

    How long to cache results in seconds

    taskQueue

    Queue where task execution happens (default: global background)

    callbackQueue

    Queue where callbacks are invoked (default: global background)

    task

    The callback-based task to execute

  • Create MonoTask with async/await execution (modern async pattern)

    Perfect for wrapping modern async APIs and Swift concurrency patterns.

    Example:

    let apiTask = MonoTask<APIResponse>(
        retry: .count(count: 2, intervalProxy: .fixed(timeInterval: 2.0)),
        resultExpireDuration: 60.0 // 1 minute
    ) {
        do {
            let response = try await APIClient.shared.fetchData()
            return .success(response)
        } catch {
            return .failure(error)
        }
    }
    

    Declaration

    Swift

    convenience init(
        retry: RetryCount = .never,
        resultExpireDuration: TimeInterval,
        taskQueue: DispatchQueue = DispatchQueue.global(),
        callbackQueue: DispatchQueue = DispatchQueue.global(),
        task: @escaping AsyncExecution
    )

    Parameters

    retry

    Retry configuration (default: no retries)

    resultExpireDuration

    How long to cache results in seconds

    taskQueue

    Queue where task execution happens (default: global background)

    callbackQueue

    Queue where callbacks are invoked (default: global background)

    task

    The async task to execute

Execution Methods

  • Execute task without waiting for result (fire-and-forget)

    Useful when you want to trigger execution but don’t need the result immediately. The task will still benefit from caching and execution merging.

    Example:

    // Pre-warm cache
    task.justExecute()
    
    // Later, this will likely return cached result
    let result = await task.asyncExecute()
    

    Declaration

    Swift

    func justExecute(forceUpdate: Bool = false)
  • Execute task with callback-based result handling

    Perfect for integrating with callback-based code or when you need to handle results in a specific queue context.

    Example:

    task.execute { result in
        switch result {
        case .success(let data):
            print("Got data: \(data)")
        case .failure(let error):
            print("Error: \(error)")
        }
    }
    

    Declaration

    Swift

    func execute(forceUpdate: Bool = false, then completionHandler: ResultCallback?)

    Parameters

    completionHandler

    Optional callback to receive the result

  • Execute task with async/await and return Result type

    This is the recommended method for modern Swift code. Returns a Result type which allows explicit error handling without exceptions.

    Example:

    let result = await task.asyncExecute()
    switch result {
    case .success(let data):
        // Handle success
        updateUI(with: data)
    case .failure(let error):
        // Handle error
        showErrorMessage(error)
    }
    

    Declaration

    Swift

    @discardableResult
    func asyncExecute(forceUpdate: Bool = false) async -> Result<TaskResult, Error>

    Return Value

    Result containing either success data or failure error

  • Execute task with async/await and throw on failure

    Convenient when you want to use Swift’s error throwing mechanism. Will throw the underlying error if execution fails.

    Example:

    do {
        let data = try await task.executeThrows()
        // Handle success case directly
        updateUI(with: data)
    } catch {
        // Handle any errors
        showErrorMessage(error)
    }
    

    Throws

    The underlying error if execution fails

    Declaration

    Swift

    @discardableResult
    func executeThrows(forceUpdate: Bool = false) async throws -> TaskResult

    Return Value

    The successful result data

State Properties

  • Get currently cached result without triggering execution

    Returns the cached result if available and not expired, otherwise nil. This property respects TTL and will return nil for expired results.

    Thread Safety: This property is thread-safe and can be called from any queue

    Use Cases:

    • Check if data is available without triggering a potentially expensive operation
    • Display cached data immediately while triggering background refresh
    • Implement cache-first UI patterns

    Example:

    // Show cached data immediately if available
    if let cached = task.currentResult {
        updateUI(with: cached)
    } else {
        showLoadingSpinner()
    }
    
    // Trigger fresh execution
    task.execute { result in
        hideLoadingSpinner()
        // Handle result...
    }
    

    Declaration

    Swift

    var currentResult: TaskResult? { get }
  • Indicates whether the task is currently executing.

    Thread-safe. Returns true while an execution is in progress (i.e., there are registered callbacks waiting for completion), and false when the task is idle.

    Declaration

    Swift

    var isExecuting: Bool { get }

Cache Management

  • Strategy for handling ongoing execution when clearing cached results

    Defines how to handle a running task when clearResult() is called.

    See more

    Declaration

    Swift

    enum OngoingExecutionStrategy
  • MonoTask-specific errors

    See more

    Declaration

    Swift

    enum Errors : Error
  • Manually invalidate cached result with fine-grained control over ongoing execution

    This method provides powerful cache management capabilities:

    Execution State Handling:

    • If task is idle: Simply clears cache, optionally starts fresh execution
    • If task is executing: Applies the chosen strategy for ongoing execution

    Strategy Details:

    • .cancel: Immediately cancels execution and notifies all callbacks with error
    • .restart: Lets execution complete, then starts fresh execution
    • .allowCompletion: Lets execution complete normally, just clears cache

    Thread Safety: Fully thread-safe, can be called from any queue

    Use Cases:

    // Force fresh data (cancel ongoing request)
    task.clearResult(ongoingExecutionStrategy: .cancel)
    
    // Clear cache but let current request complete
    task.clearResult(ongoingExecutionStrategy: .allowCompletion)
    
    // Clear cache and ensure fresh execution
    task.clearResult(ongoingExecutionStrategy: .restart, shouldRestartWhenIDLE: true)
    

    Declaration

    Swift

    func clearResult(
        ongoingExecutionStrategy: OngoingExecutionStrategy = .allowCompletion,
        shouldRestartWhenIDLE: Bool = false
    )

    Parameters

    ongoingExecutionStrategy

    How to handle currently running execution

    shouldRestartWhenIDLE

    Whether to start new execution if task is idle