-
Notifications
You must be signed in to change notification settings - Fork 57
Expand file tree
/
Copy pathPromise.swift
More file actions
341 lines (294 loc) · 11.2 KB
/
Promise.swift
File metadata and controls
341 lines (294 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
//
// Promise.swift
// Promise
//
// Created by Soroush Khanlou on 7/21/16.
//
//
import Foundation
#if os(Linux)
import Dispatch
#endif
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 var 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) -> Void
let onRejected: (Error) -> Void
let executionContext: ExecutionContext
func callFulfill(_ value: Value, completion: @escaping () -> Void = { }) {
executionContext.execute({
self.onFulfilled(value)
completion()
})
}
func callReject(_ error: Error, completion: @escaping () -> Void = { }) {
executionContext.execute({
self.onRejected(error)
completion()
})
}
}
enum State<Value>: CustomStringConvertible {
/// The promise has not completed yet.
/// Will transition to either the `fulfilled` or `rejected` state.
case pending(callbacks: [Callback<Value>])
/// 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)
/// Creates a Promise in the pending state which can be fulfilled or rejected.
public init() {
state = .pending(callbacks: [])
}
/// Creates a Promise that is immediately fulfilled.
/// - Parameter value: Result of this Promise
public init(value: Value) {
state = .fulfilled(value: value)
}
/// Creates a Promise that is immediately rejected.
/// - Parameter error: Result of this Promise
public init(error: Error) {
state = .rejected(error: error)
}
/// Typical Promise intializer. Initializes this Promise with a block
/// of work to be performed, and allows that closure to either fulfill
/// or reject the promise.
/// - Parameters:
/// - queue: Optional; queue to perform the work on. Defaults to the
/// global queue with the `.userInitiated` quality of service.
/// - work: Work to be performed. If the work is succesful, pass the
/// result to `fulfill()` closure. If the work fails, pass the
/// error to `reject()`.
/// - fulfill: Fulfills this promise with the given value.
/// - reject: Rejects this promise with the given error.
///
/// Promises are *fulfilled* if they complete successfully with a value
/// of the type of the Promise. A `Promise<String>` is *fulfilled* if it
/// generates a `String`.
///
/// Promises are *rejected* if they fail, and thus, generate an `Error`.
/// With this library, any Promise can produce any `Error`
public convenience init(queue: DispatchQueue = DispatchQueue.global(qos: .userInitiated), work: @escaping (_ fulfill: @escaping (Value) -> Void, _ reject: @escaping (Error) -> Void) throws -> Void) {
self.init()
queue.async(execute: {
do {
try work(self.fulfill, self.reject)
} catch let error {
self.reject(error)
}
})
}
/// Converts the result of this promise to a new value
/// of type `NewValue` by returning a `Promise<NewValue>`
///
/// - Parameter queue: Optional; queue to perform the work on.
/// Defaults to the main queue.
/// - Parameter onFulfilled: Transform to perform if this closure fulfills.
/// - Returns: `Promise<NewValue>`
///
/// This is roughly the equivalent of a traditional `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(on: queue, fulfill, reject)
} catch let error {
reject(error)
}
},
onRejected: reject
)
})
}
/// Converts the result of this promise to a new value
/// of type `NewValue` by returning an instance of `NewValue`.
///
/// - Parameters:
/// - queue: Optional; queue to perform the work on.
/// Defaults to the main queue.
/// - onFulfilled: Transform to perform if this closure fulfills.
/// - Returns: `Promise<NewValue>`
///
/// This is roughly the equivalent of a traditional `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)
}
})
}
/// Handles the completion of a Promise.
/// - Parameters:
/// - queue: Optional; queue to perform this work on.
/// Defaults to the main queue.
/// - onFulfilled: Work to perform if the Promise is fulfilled (completes with a value)
/// - onRejected: Work to perform if the Promise is rejected (completes with an error)
/// - Returns: A discardable instance of this promise that can be used for further chaining.
@discardableResult
public func then(on queue: ExecutionContext = DispatchQueue.main, _ onFulfilled: @escaping (Value) -> Void, _ onRejected: @escaping (Error) -> Void = { _ in }) -> Promise<Value> {
addCallbacks(on: queue, onFulfilled: onFulfilled, onRejected: onRejected)
return self
}
/// Catches an error if this promise chain is rejected and performs some work.
/// - Parameters:
/// - queue: Optional; queue to perform this work on.
/// Defaults to the main queue.
/// - onRejected: Work to perform if this promise chain is rejected.
/// - Returns: A discardable instance of this promise that can be used for further chaining.
@discardableResult
public func `catch`(on queue: ExecutionContext = DispatchQueue.main, _ onRejected: @escaping (Error) -> Void) -> Promise<Value> {
return then(on: queue, { _ in }, onRejected)
}
/// Rejects this Promise (completes it with an `Error`).
/// - Parameter error: Error to reject with
public func reject(_ error: Error) {
updateState(.rejected(error: error))
}
/// Fulfills this Promise (completes it succesfully with an instance of `Value`).
/// - Parameter value: Instance of `Value` to fulfill with
public func fulfill(_ value: Value) {
updateState(.fulfilled(value: value))
}
/// A flag indicating if the promise is still pending.
public var isPending: Bool {
return !isFulfilled && !isRejected
}
/// A flag indicating if the promise is fulfilled (completed successfully).
public var isFulfilled: Bool {
return value != nil
}
/// A flag indicating if the promise is rejected (completed with failure).
public var isRejected: Bool {
return error != nil
}
/// The value that the promise was fulfilled with, if it was fulfilled. `nil` otherwise.
public var value: Value? {
return lockQueue.sync(execute: {
return self.state.value
})
}
/// The `Error` that the promise was rejected with, if it was rejected. `nil` otherwise.
public var error: Error? {
return lockQueue.sync(execute: {
return self.state.error
})
}
private func updateState(_ newState: State<Value>) {
lockQueue.async(execute: {
guard case .pending(let callbacks) = self.state else { return }
self.state = newState
self.fireIfCompleted(callbacks: callbacks)
})
}
private func addCallbacks(on queue: ExecutionContext = DispatchQueue.main, onFulfilled: @escaping (Value) -> Void, onRejected: @escaping (Error) -> Void) {
let callback = Callback(onFulfilled: onFulfilled, onRejected: onRejected, executionContext: queue)
lockQueue.async(flags: .barrier, execute: {
switch self.state {
case .pending(let callbacks):
self.state = .pending(callbacks: callbacks + [callback])
case .fulfilled(let value):
callback.callFulfill(value)
case .rejected(let error):
callback.callReject(error)
}
})
}
private func fireIfCompleted(callbacks: [Callback<Value>]) {
guard !callbacks.isEmpty else {
return
}
lockQueue.async(execute: {
switch self.state {
case .pending:
break
case let .fulfilled(value):
var mutableCallbacks = callbacks
let firstCallback = mutableCallbacks.removeFirst()
firstCallback.callFulfill(value, completion: {
self.fireIfCompleted(callbacks: mutableCallbacks)
})
case let .rejected(error):
var mutableCallbacks = callbacks
let firstCallback = mutableCallbacks.removeFirst()
firstCallback.callReject(error, completion: {
self.fireIfCompleted(callbacks: mutableCallbacks)
})
}
})
}
}