Permalink
Find file
996d670 Jan 14, 2017
@khanlou @kelan
274 lines (227 sloc) 7.09 KB
//
// Promise.swift
// Promise
//
// Created by Soroush Khanlou on 7/21/16.
//
//
import Foundation
public protocol ExecutionContext {
func execute(_ work: @escaping () -> Void)
}
extension DispatchQueue: ExecutionContext {
public func execute(_ work: @escaping () -> Void) {
self.async(execute: work)
}
}
public final class InvalidatableQueue: ExecutionContext {
private var valid = true
private let queue: DispatchQueue
public init(queue: DispatchQueue = .main) {
self.queue = queue
}
public func invalidate() {
valid = false
}
public func execute(_ work: @escaping () -> Void) {
guard valid else { return }
self.queue.async(execute: work)
}
}
struct Callback<Value> {
let onFulfilled: (Value) -> ()
let onRejected: (Error) -> ()
let queue: ExecutionContext
func callFulfill(_ value: Value) {
queue.execute({
self.onFulfilled(value)
})
}
func callReject(_ error: Error) {
queue.execute({
self.onRejected(error)
})
}
}
enum State<Value>: CustomStringConvertible {
/// The promise has not completed yet.
/// Will transition to either the `fulfilled` or `rejected` state.
case pending
/// The promise now has a value.
/// Will not transition to any other state.
case fulfilled(value: Value)
/// The promise failed with the included error.
/// Will not transition to any other state.
case rejected(error: Error)
var isPending: Bool {
if case .pending = self {
return true
} else {
return false
}
}
var isFulfilled: Bool {
if case .fulfilled = self {
return true
} else {
return false
}
}
var isRejected: Bool {
if case .rejected = self {
return true
} else {
return false
}
}
var value: Value? {
if case let .fulfilled(value) = self {
return value
}
return nil
}
var error: Error? {
if case let .rejected(error) = self {
return error
}
return nil
}
var description: String {
switch self {
case .fulfilled(let value):
return "Fulfilled (\(value))"
case .rejected(let error):
return "Rejected (\(error))"
case .pending:
return "Pending"
}
}
}
public final class Promise<Value> {
private var state: State<Value>
private let lockQueue = DispatchQueue(label: "promise_lock_queue", qos: .userInitiated)
private var callbacks: [Callback<Value>] = []
public init() {
state = .pending
}
public init(value: Value) {
state = .fulfilled(value: value)
}
public init(error: Error) {
state = .rejected(error: error)
}
public convenience init(queue: DispatchQueue = DispatchQueue.global(qos: .userInitiated), work: @escaping (_ fulfill: @escaping (Value) -> (), _ reject: @escaping (Error) -> () ) throws -> ()) {
self.init()
queue.async(execute: {
do {
try work(self.fulfill, self.reject)
} catch let error {
self.reject(error)
}
})
}
/// - note: This one is "flatMap"
@discardableResult
public func then<NewValue>(on queue: ExecutionContext = DispatchQueue.main, _ onFulfilled: @escaping (Value) throws -> Promise<NewValue>) -> Promise<NewValue> {
return Promise<NewValue>(work: { fulfill, reject in
self.addCallbacks(
on: queue,
onFulfilled: { value in
do {
try onFulfilled(value).then(fulfill, reject)
} catch let error {
reject(error)
}
},
onRejected: reject
)
})
}
/// - note: This one is "map"
@discardableResult
public func then<NewValue>(on queue: ExecutionContext = DispatchQueue.main, _ onFulfilled: @escaping (Value) throws -> NewValue) -> Promise<NewValue> {
return then(on: queue, { (value) -> Promise<NewValue> in
do {
return Promise<NewValue>(value: try onFulfilled(value))
} catch let error {
return Promise<NewValue>(error: error)
}
})
}
@discardableResult
public func then(on queue: ExecutionContext = DispatchQueue.main, _ onFulfilled: @escaping (Value) -> (), _ onRejected: @escaping (Error) -> () = { _ in }) -> Promise<Value> {
_ = Promise<Value>(work: { fulfill, reject in
self.addCallbacks(
on: queue,
onFulfilled: { value in
fulfill(value)
onFulfilled(value)
},
onRejected: { error in
reject(error)
onRejected(error)
}
)
})
return self
}
@discardableResult
public func `catch`(on queue: ExecutionContext = DispatchQueue.main, _ onRejected: @escaping (Error) -> ()) -> Promise<Value> {
return then(on: queue, { _ in }, onRejected)
}
public func reject(_ error: Error) {
updateState(.rejected(error: error))
}
public func fulfill(_ value: Value) {
updateState(.fulfilled(value: value))
}
public var isPending: Bool {
return !isFulfilled && !isRejected
}
public var isFulfilled: Bool {
return value != nil
}
public var isRejected: Bool {
return error != nil
}
public var value: Value? {
return lockQueue.sync(execute: {
return self.state.value
})
}
public var error: Error? {
return lockQueue.sync(execute: {
return self.state.error
})
}
private func updateState(_ state: State<Value>) {
guard self.isPending else { return }
lockQueue.sync(execute: {
self.state = state
})
fireCallbacksIfCompleted()
}
private func addCallbacks(on queue: ExecutionContext = DispatchQueue.main, onFulfilled: @escaping (Value) -> (), onRejected: @escaping (Error) -> ()) {
let callback = Callback(onFulfilled: onFulfilled, onRejected: onRejected, queue: queue)
lockQueue.async(execute: {
self.callbacks.append(callback)
})
fireCallbacksIfCompleted()
}
private func fireCallbacksIfCompleted() {
lockQueue.async(execute: {
guard !self.state.isPending else { return }
self.callbacks.forEach { callback in
switch self.state {
case let .fulfilled(value):
callback.callFulfill(value)
case let .rejected(error):
callback.callReject(error)
default:
break
}
}
self.callbacks.removeAll()
})
}
}