Sources/BoltsSwift/Task.swift (185 lines of code) (raw):
/*
* Copyright (c) 2016, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
import Foundation
enum TaskState<TResult> {
case pending
case success(TResult)
case error(Error)
case cancelled
static func fromClosure(_ closure: () throws -> TResult) -> TaskState {
do {
return .success(try closure())
} catch is CancelledError {
return .cancelled
} catch {
return .error(error)
}
}
}
struct TaskContinuationOptions: OptionSet {
let rawValue: Int
init(rawValue: Int) {
self.rawValue = rawValue
}
static let RunOnSuccess = TaskContinuationOptions(rawValue: 1 << 0)
static let RunOnError = TaskContinuationOptions(rawValue: 1 << 1)
static let RunOnCancelled = TaskContinuationOptions(rawValue: 1 << 2)
static let RunAlways: TaskContinuationOptions = [ .RunOnSuccess, .RunOnError, .RunOnCancelled ]
}
//--------------------------------------
// MARK: - Task
//--------------------------------------
///
/// The consumer view of a Task.
/// Task has methods to inspect the state of the task, and to add continuations to be run once the task is complete.
///
public final class Task<TResult> {
public typealias Continuation = () -> Void
fileprivate let synchronizationQueue = DispatchQueue(label: "com.bolts.task", attributes: DispatchQueue.Attributes.concurrent)
fileprivate var _completedCondition: NSCondition?
fileprivate var _state: TaskState<TResult> = .pending
fileprivate var _continuations: [Continuation] = Array()
// MARK: Initializers
init() {}
init(state: TaskState<TResult>) {
_state = state
}
/**
Creates a task that is already completed with the given result.
- parameter result: The task result.
*/
public init(_ result: TResult) {
_state = .success(result)
}
/**
Initializes a task that is already completed with the given error.
- parameter error: The task error.
*/
public init(error: Error) {
_state = .error(error)
}
/**
Creates a cancelled task.
- returns: A cancelled task.
*/
public class func cancelledTask() -> Self {
// Swift prevents this method from being called `cancelled` due to the `cancelled` instance var. This is most likely a bug.
return self.init(state: .cancelled)
}
class func emptyTask() -> Task<Void> {
return Task<Void>(state: .success(()))
}
// MARK: Execute
/**
Creates a task that will complete with the result of the given closure.
- note: The closure cannot make the returned task to fail. Use the other `execute` overload for this.
- parameter executor: Determines how the the closure is called. The default is to call the closure immediately.
- parameter closure: The closure that returns the result of the task.
The returned task will complete when the closure completes.
*/
public convenience init(_ executor: Executor = .default, closure: @escaping (() throws -> TResult)) {
self.init(state: .pending)
executor.execute {
self.trySet(state: TaskState.fromClosure(closure))
}
}
/**
Creates a task that will continue with the task returned by the given closure.
- parameter executor: Determines how the the closure is called. The default is to call the closure immediately.
- parameter closure: The closure that returns the continuation task.
The returned task will complete when the continuation task completes.
- returns: A task that will continue with the task returned by the given closure.
*/
public class func execute(_ executor: Executor = .default, closure: @escaping (() throws -> TResult)) -> Task {
return Task(executor, closure: closure)
}
/**
Creates a task that will continue with the task returned by the given closure.
- parameter executor: Determines how the the closure is called. The default is to call the closure immediately.
- parameter closure: The closure that returns the continuation task.
The returned task will complete when the continuation task completes.
- returns: A task that will continue with the task returned by the given closure.
*/
public class func executeWithTask(_ executor: Executor = .default, closure: @escaping (() throws -> Task)) -> Task {
return emptyTask().continueWithTask(executor) { _ in
return try closure()
}
}
// MARK: State Accessors
/// Whether this task is completed. A completed task can also be faulted or cancelled.
public var completed: Bool {
switch state {
case .pending:
return false
default:
return true
}
}
/// Whether this task has completed due to an error or exception. A `faulted` task is also completed.
public var faulted: Bool {
switch state {
case .error:
return true
default:
return false
}
}
/// Whether this task has been cancelled. A `cancelled` task is also completed.
public var cancelled: Bool {
switch state {
case .cancelled:
return true
default:
return false
}
}
/// The result of a successful task. Won't be set until the task completes with a `result`.
public var result: TResult? {
switch state {
case .success(let result):
return result
default:
break
}
return nil
}
/// The error of a errored task. Won't be set until the task completes with `error`.
public var error: Error? {
switch state {
case .error(let error):
return error
default:
break
}
return nil
}
/**
Waits until this operation is completed.
This method is inefficient and consumes a thread resource while it's running.
It should be avoided. This method logs a warning message if it is used on the main thread.
*/
public func waitUntilCompleted() {
if Thread.isMainThread {
debugPrint("Warning: A long-running operation is being executed on the main thread waiting on \(self).")
}
var conditon: NSCondition?
synchronizationQueue.sync(flags: .barrier, execute: {
if case .pending = self._state {
conditon = self._completedCondition ?? NSCondition()
self._completedCondition = conditon
}
})
guard let condition = conditon else {
// Task should have been completed
precondition(completed)
return
}
condition.lock()
while !completed {
condition.wait()
}
condition.unlock()
synchronizationQueue.sync(flags: .barrier, execute: {
self._completedCondition = nil
})
}
// MARK: State Change
@discardableResult
func trySet(state: TaskState<TResult>) -> Bool {
var stateChanged = false
var continuations: [Continuation]?
var completedCondition: NSCondition?
synchronizationQueue.sync(flags: .barrier, execute: {
switch self._state {
case .pending:
stateChanged = true
self._state = state
continuations = self._continuations
completedCondition = self._completedCondition
self._continuations.removeAll()
default:
break
}
})
if stateChanged {
completedCondition?.lock()
completedCondition?.broadcast()
completedCondition?.unlock()
for continuation in continuations! {
continuation()
}
}
return stateChanged
}
// MARK: Internal
func appendOrRunContinuation(_ continuation: @escaping Continuation) {
var runContinuation = false
synchronizationQueue.sync(flags: .barrier, execute: {
switch self._state {
case .pending:
self._continuations.append(continuation)
default:
runContinuation = true
}
})
if runContinuation {
continuation()
}
}
var state: TaskState<TResult> {
var value: TaskState<TResult>?
synchronizationQueue.sync {
value = self._state
}
return value!
}
}
//--------------------------------------
// MARK: - Description
//--------------------------------------
extension Task: CustomStringConvertible, CustomDebugStringConvertible {
/// A textual representation of `self`.
public var description: String {
return "Task: \(self.state)"
}
/// A textual representation of `self`, suitable for debugging.
public var debugDescription: String {
return "Task: \(self.state)"
}
}