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.
-
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
AsynchronousSwift 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
-
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
-
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
-
asyncExecute(forceUpdate:
Asynchronous) 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
-
executeThrows(forceUpdate:
Asynchronous) 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 failsDeclaration
Swift
@discardableResult func executeThrows(forceUpdate: Bool = false) async throws -> TaskResult
Return Value
The successful result data
-
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 }
-
Strategy for handling ongoing execution when clearing cached results
Defines how to handle a running task when
See moreclearResult()
is called.Declaration
Swift
enum OngoingExecutionStrategy
-
MonoTask-specific errors
See moreDeclaration
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