| // Copyright 2016 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| import Foundation |
| |
| public enum PromiseErrors<ResolveType>: ErrorType { |
| case AlreadyResolved(existingObj:ResolveType) |
| case AlreadyRejected(existingErr:ErrorType?) |
| case Timeout |
| } |
| |
| public enum PromiseResolution<ResolveType> { |
| case Pending |
| case Resolved(obj:ResolveType) |
| case Rejected(err:ErrorType?) |
| } |
| |
| public class Promise<ResolveType> : Lockable { |
| |
| private var resolveCallbacks:[(dispatch_queue_t?, ResolveType->())]? = nil |
| private var rejectCallbacks:[(dispatch_queue_t?, ErrorType?->())]? = nil |
| private var alwaysCallbacks:[(dispatch_queue_t?, PromiseResolution<ResolveType>->())]? = nil |
| private let resolutionSemaphore = dispatch_semaphore_create(0) |
| |
| internal var status: PromiseResolution<ResolveType> = PromiseResolution.Pending { |
| didSet { |
| updatedStatus() |
| } |
| } |
| |
| public init() { } |
| |
| public convenience init(resolved obj: ResolveType) { |
| self.init() |
| status = PromiseResolution<ResolveType>.Resolved(obj: obj) |
| } |
| |
| public convenience init(rejected err: ErrorType?) { |
| self.init() |
| status = PromiseResolution<ResolveType>.Rejected(err: err) |
| } |
| |
| // Resolve/reject |
| public func resolve(obj:ResolveType) throws { |
| objc_sync_enter(self) |
| defer { objc_sync_exit(self) } |
| switch (status) { |
| case .Rejected(let err): throw PromiseErrors<ResolveType>.AlreadyRejected(existingErr: err) |
| case .Resolved(let obj): throw PromiseErrors<ResolveType>.AlreadyResolved(existingObj: obj) |
| case .Pending: status = PromiseResolution.Resolved(obj: obj) |
| } |
| } |
| |
| public func reject(err:ErrorType?) throws { |
| objc_sync_enter(self) |
| defer { objc_sync_exit(self) } |
| switch (status) { |
| case .Rejected(let err): throw PromiseErrors<ResolveType>.AlreadyRejected(existingErr: err) |
| case .Resolved(let obj): throw PromiseErrors<ResolveType>.AlreadyResolved(existingObj: obj) |
| case .Pending: status = PromiseResolution.Rejected(err: err) |
| } |
| } |
| |
| // Chain/handle state |
| public func onResolve(on queue: dispatch_queue_t?=dispatch_get_main_queue(), |
| _ callback:ResolveType->()) -> Promise<ResolveType> { |
| objc_sync_enter(self) |
| defer { objc_sync_exit(self) } |
| switch(status) { |
| case .Resolved(let obj): dispatch_maybe_async(queue, block: { callback(obj) }) |
| case .Rejected: break |
| case .Pending: |
| if resolveCallbacks == nil { |
| resolveCallbacks = [] |
| } |
| resolveCallbacks!.append((queue, callback)) |
| } |
| |
| return self |
| } |
| |
| public func onReject(on queue: dispatch_queue_t?=dispatch_get_main_queue(), |
| _ callback:ErrorType?->()) -> Promise<ResolveType> { |
| objc_sync_enter(self) |
| defer { objc_sync_exit(self) } |
| switch(status) { |
| case .Resolved: break |
| case .Rejected(let err): dispatch_maybe_async(queue, block: { callback(err) }) |
| case .Pending: |
| if rejectCallbacks == nil { |
| rejectCallbacks = [] |
| } |
| rejectCallbacks!.append((queue, callback)) |
| } |
| return self |
| } |
| |
| public func always(on queue: dispatch_queue_t?=dispatch_get_main_queue(), |
| _ callback:PromiseResolution<ResolveType>->()) -> Promise<ResolveType> { |
| objc_sync_enter(self) |
| defer { objc_sync_exit(self) } |
| let s = status |
| switch(s) { |
| case .Pending: |
| if alwaysCallbacks == nil { |
| alwaysCallbacks = [] |
| } |
| alwaysCallbacks!.append((queue, callback)) |
| default: dispatch_maybe_async(queue, block: { callback(s) }) |
| } |
| return self |
| } |
| |
| public func await(timeout: NSTimeInterval?=nil) throws -> PromiseResolution<ResolveType> { |
| switch (status) { |
| case .Pending: |
| if let timeout = timeout { |
| if dispatch_semaphore_wait(resolutionSemaphore, dispatch_time_t.fromNSTimeInterval(timeout)) != 0 { |
| throw PromiseErrors<ResolveType>.Timeout |
| } |
| } else { |
| dispatch_semaphore_wait(resolutionSemaphore, DISPATCH_TIME_FOREVER) |
| } |
| default: break |
| } |
| return status |
| } |
| |
| // Changing states |
| internal func updatedStatus() { |
| // Do reject/resolve callbacks, then always callbacks |
| let s = status |
| switch (s) { |
| case .Pending: return // Don't do anything, but this shouldn't happen |
| case .Rejected(let err): |
| guard let callbacks = rejectCallbacks else { break } |
| for (queue, callback) in callbacks { |
| dispatch_maybe_async(queue) { |
| callback(err) |
| } |
| } |
| case .Resolved(let obj): |
| guard let callbacks = resolveCallbacks else { break } |
| for (queue, callback) in callbacks { |
| dispatch_maybe_async(queue) { |
| callback(obj) |
| } |
| } |
| } |
| |
| defer { dispatch_semaphore_signal(resolutionSemaphore) } |
| guard let callbacks = alwaysCallbacks else { return } |
| for (queue, callback) in callbacks { |
| dispatch_maybe_async(queue) { |
| callback(s) |
| } |
| } |
| } |
| } |
| |
| public class ResolvedPromise<ResolveType>: Promise<ResolveType> { |
| var resolvedObj:ResolveType |
| |
| override private init() { |
| fatalError("Cannot init this way") |
| } |
| |
| public convenience init(_ resolved: ResolveType) { |
| self.init(resolved: resolved) |
| self.resolvedObj = resolved |
| dispatch_semaphore_signal(resolutionSemaphore) |
| } |
| |
| override public func onResolve(on queue: dispatch_queue_t?=dispatch_get_main_queue(), |
| _ callback:ResolveType->()) -> Promise<ResolveType> { |
| dispatch_maybe_async(queue, block: { callback(self.resolvedObj) }) |
| return self |
| } |
| |
| override public func always(on queue: dispatch_queue_t?=dispatch_get_main_queue(), |
| _ callback:PromiseResolution<ResolveType>->()) -> Promise<ResolveType> { |
| dispatch_maybe_async(queue, block: { callback(self.status) }) |
| return self |
| } |
| |
| override public func resolve(obj:ResolveType) throws { |
| throw PromiseErrors<ResolveType>.AlreadyResolved(existingObj: resolvedObj) |
| } |
| |
| override public func reject(err:ErrorType?) throws { |
| throw PromiseErrors<ResolveType>.AlreadyResolved(existingObj: resolvedObj) |
| } |
| } |
| |
| public class RejectedPromise: Promise<Void> { |
| var err:ErrorType? |
| |
| override private init() { |
| fatalError("Cannot init this way") |
| } |
| |
| public convenience init(_ error: ErrorType?) { |
| self.init(rejected: error) |
| self.err = error |
| dispatch_semaphore_signal(resolutionSemaphore) |
| } |
| |
| override public func onReject(on queue: dispatch_queue_t?, _ callback: ErrorType? -> ()) -> Promise<Void> { |
| dispatch_maybe_async(queue, block: { callback(self.err) }) |
| return self |
| } |
| |
| override public func always(on queue: dispatch_queue_t?=dispatch_get_main_queue(), |
| _ callback:PromiseResolution<Void>->()) -> Promise<Void> { |
| dispatch_maybe_async(queue, block: { callback(self.status) }) |
| return self |
| } |
| |
| override public func resolve(obj:Void) throws { |
| throw PromiseErrors<Void>.AlreadyRejected(existingErr: self.err) |
| } |
| |
| override public func reject(err:ErrorType?) throws { |
| throw PromiseErrors<Void>.AlreadyRejected(existingErr: self.err) |
| } |
| } |
| |
| /// Quick initializers for already resolved/rejected constructors |
| public extension Promise { |
| public static func resolved() -> Promise<Void> { return Promise<Void>(resolved: ()) } |
| public static func resolved(obj:ResolveType) -> Promise<ResolveType> { return ResolvedPromise(obj) } |
| public static func rejected(err:ErrorType? = nil) -> Promise<Void> { return RejectedPromise(err) } |
| } |
| |
| /// Chaining API |
| public extension Promise { |
| func then<T>(on queue: dispatch_queue_t?=dispatch_get_main_queue(), |
| _ callback:ResolveType throws ->T) -> Promise<T> { |
| let p = Promise<T>() |
| onResolve(on: queue) { obj in |
| do { |
| let transformed = try callback(obj) |
| try p.resolve(transformed) |
| } catch let e { |
| try! p.reject(e) |
| } |
| } |
| onReject(on: queue) { err in |
| try! p.reject(err) |
| } |
| return p |
| } |
| |
| /// Specialize then for when it returns another Promise to hook onto that automatically |
| func then<T>(on queue: dispatch_queue_t?=dispatch_get_main_queue(), |
| _ callback:ResolveType throws ->Promise<T>) -> Promise<T> { |
| let p = Promise<T>() |
| |
| onResolve(on: queue) { obj in |
| do { |
| let newPromise = try callback(obj) |
| newPromise.onResolve(on: queue) { obj in |
| do { |
| try p.resolve(obj) |
| } catch let e { |
| try! p.reject(e) |
| } |
| } |
| newPromise.onReject(on: queue) { err in |
| try! p.reject(err) |
| } |
| } catch let e { |
| try! p.reject(e) |
| } |
| } |
| |
| onReject(on: queue) { err in |
| try! p.reject(err) |
| } |
| return p |
| } |
| |
| func thenInBackground<T>(callback:ResolveType throws ->T) -> Promise<T> { |
| return then(on: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), callback) |
| } |
| |
| func thenOnSameThread<T>(callback:ResolveType throws ->T) -> Promise<T> { |
| return then(on: nil, callback) |
| } |
| } |
| |
| /// Visibility into status |
| public extension Promise { |
| public func isPending() -> Bool { |
| switch (status) { |
| case .Pending: return true |
| default: return false |
| } |
| } |
| |
| public func isResolved() -> Bool { |
| switch (status) { |
| case .Resolved: return true |
| default: return false |
| } |
| } |
| |
| public func isRejected() -> Bool { |
| switch (status) { |
| case .Rejected: return true |
| default: return false |
| } |
| } |
| } |
| |
| /// Utilities for timeout functionality |
| public extension Promise { |
| /// Timeout (reject) if not resolved or rejected within a given timeframe |
| public func rejectAfterDelay(on queue:dispatch_queue_t=dispatch_get_main_queue(), delay:NSTimeInterval) { |
| dispatch_after_delay(delay, queue: queue, block: { |
| switch (self.status) { |
| case .Pending: |
| do { try self.reject(PromiseErrors<ResolveType>.Timeout) } |
| catch { } |
| default: break |
| } |
| }) |
| } |
| } |