|
1 | 1 | # AsyncExtensions |
2 | 2 |
|
3 | | -AsyncExtensions mimics combine operators for async sequences. |
| 3 | + |
| 4 | +<p align="left"> |
| 5 | +<img src="https://github.com/AsyncCommunity/AsyncExtensions/actions/workflows/ci.yml/badge.svg?branch=main" alt="Build Status" title="Build Status"> |
| 6 | +<a href="https://codecov.io/gh/AsyncCommunity/AsyncExtensions"><img src="https://codecov.io/gh/AsyncCommunity/AsyncExtensions/branch/main/graph/badge.svg?token=NTGOIK6CSE"/></a> |
| 7 | +<a href="https://github.com/apple/swift-package-manager" target="_blank"><img src="https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg" alt="CombineExt supports Swift Package Manager (SPM)"></a> |
| 8 | +<img src="https://img.shields.io/badge/platforms-iOS%2013%20%7C%20macOS 10.15%20%7C%20tvOS%2013%20%7C%20watchOS%206-333333.svg" /> |
| 9 | + |
| 10 | +AsyncExtensions provides a collection of operators, async sequences and async streams that mimics Combine behaviour. |
| 11 | + |
| 12 | +The purpose is to be able to chain operators, just as you would do with any reactive programming framework: |
| 13 | + |
| 14 | +```swift |
| 15 | +AsyncSequences |
| 16 | + .Merge(sequence1, sequence2, sequence3) |
| 17 | + .prepend(0) |
| 18 | + .handleEvents(onElement: { print($0) }, onFinish: { print("Finished") }) |
| 19 | + .scan("") { accumulator, element in accumulator + "\(element)" } |
| 20 | + .collect { print($0) } |
| 21 | +``` |
| 22 | + |
| 23 | +### Async Sequences |
| 24 | +* [Just](#Just) |
| 25 | +* [Empty](#Empty) |
| 26 | +* [Fail](#Fail) |
| 27 | +* [From](#From) |
| 28 | +* [Merge](#Merge) |
| 29 | +* [Zip2](#Zip2) |
| 30 | +* [Zip3](#Zip3) |
| 31 | +* [Zip](#Zip) |
| 32 | + |
| 33 | +### Async Streams |
| 34 | +* [Passthrough](#Passthrough) |
| 35 | +* [CurrentValue](#CurrentValue) |
| 36 | +* [Replay](#Replay) |
| 37 | + |
| 38 | +### Operators |
| 39 | +* [Collect](#Collect) |
| 40 | +* [Scan](#Scan) |
| 41 | +* [SwitchToLatest](#SwitchToLatest) |
| 42 | +* [FlatMapLatest](#FlatMapLatest) |
| 43 | +* [HandleEvents](#HandleEvents) |
| 44 | +* [EraseToAnyAsyncSequence](#EraseToAnyAsyncSequence) |
| 45 | + |
| 46 | +More operators and extensions are to come. Pull requests are of course welcome. |
| 47 | + |
| 48 | +## Async Sequences |
| 49 | + |
| 50 | +### Just |
| 51 | + |
| 52 | +`Just` is an AsyncSequence that outputs a single value and finishes. |
| 53 | + |
| 54 | +```swift |
| 55 | +let justSequence = AsyncSequences.Just<Int>(1) |
| 56 | +for try await element in justSequence { |
| 57 | + // will be called once with element = 1 |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +### Empty |
| 62 | + |
| 63 | +`Empty` is an AsyncSequence that immediately finishes without emitting values. |
| 64 | + |
| 65 | +```swift |
| 66 | +let emptySequence = AsyncSequences.Empty<Int>() |
| 67 | +for try await element in emptySequence { |
| 68 | + // will never be called |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +### Fail |
| 73 | + |
| 74 | +`Fail` is an AsyncSequence that outputs no elements and throws an error. |
| 75 | + |
| 76 | +```swift |
| 77 | +let failSequence = AsyncSequences.Fail<Int, Swift.Error>(error: NSError(domain: "", code: 1)) |
| 78 | +do { |
| 79 | + for try await element in failSequence { |
| 80 | + // will never be called |
| 81 | + } |
| 82 | +} catch { |
| 83 | + // will catch `NSError(domain: "", code: 1)` here |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +### From |
| 88 | + |
| 89 | +`From` is an AsyncSequence that outputs elements from a traditional Sequence. |
| 90 | + |
| 91 | +```swift |
| 92 | +let fromSequence = AsyncSequences.From([1, 2, 3, 4, 5]) |
| 93 | + |
| 94 | +for await element in fromSequence { |
| 95 | + print(element) // will print 1 2 3 4 5 |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +A variation offers to set an interval of time between each element. |
| 100 | + |
| 101 | +```swift |
| 102 | +let fromSequence = AsyncSequences.From([1, 2, 3, 4, 5], interval: .milliSeconds(10)) |
| 103 | + |
| 104 | +for await element in fromSequence { |
| 105 | + print(element) // will print 1 2 3 4 5 with an interval of 10ms between elements |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +### Merge |
| 110 | + |
| 111 | +`Merge` is an AsyncSequence that merges several async sequences respecting their temporality while being iterated over. |
| 112 | +When all the async sequences have finished, so too does the merged async sequence. |
| 113 | +If an async sequence fails, so too does the merged async sequence. |
| 114 | + |
| 115 | +```swift |
| 116 | +// 0.1ms 1ms 1.5ms 2ms 3ms 4.5ms |
| 117 | +// 4 1 5 2 3 6 |
| 118 | + |
| 119 | +let asyncSequence1 = AsyncStream(Int.self, bufferingPolicy: .unbounded) { continuation in |
| 120 | + Task { |
| 121 | + try await Task.sleep(nanoseconds: 1_000_000) |
| 122 | + continuation.yield(1) |
| 123 | + try await Task.sleep(nanoseconds: 1_000_000) |
| 124 | + continuation.yield(2) |
| 125 | + try await Task.sleep(nanoseconds: 1_000_000) |
| 126 | + continuation.yield(3) |
| 127 | + continuation.finish() |
| 128 | + } |
| 129 | +} |
| 130 | + |
| 131 | +let asyncSequence2 = AsyncStream(Int.self, bufferingPolicy: .unbounded) { continuation in |
| 132 | + Task { |
| 133 | + try await Task.sleep(nanoseconds: 100_000) |
| 134 | + continuation.yield(4) |
| 135 | + try await Task.sleep(nanoseconds: 1_400_000) |
| 136 | + continuation.yield(5) |
| 137 | + try await Task.sleep(nanoseconds: 3_000_000) |
| 138 | + continuation.yield(6) |
| 139 | + continuation.finish() |
| 140 | + } |
| 141 | +} |
| 142 | + |
| 143 | +let mergedAsyncSequence = AsyncSequences.Merge(asyncSequence1, asyncSequence2) |
| 144 | + |
| 145 | +for try await element in mergedAsyncSequence { |
| 146 | + print(element) // will print -> 4 1 5 2 3 6 |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +### Zip2 |
| 151 | + |
| 152 | +`Zip2` is an AsyncSequence that combines the latest elements from two sequences according to their temporality and emits a tuple to the client. |
| 153 | +If any async sequence ends successfully or fails with an error, so too does the zipped async Sequence. |
| 154 | + |
| 155 | +```swift |
| 156 | +let asyncSequence1 = AsyncSequences.From([1, 2, 3, 4, 5]) |
| 157 | +let asyncSequence2 = AsyncSequences.From(["5", "4", "3", "2", "1"]) |
| 158 | + |
| 159 | +let zippedAsyncSequence = AsyncSequences.Zip2(asyncSequence1, asyncSequence2) |
| 160 | + |
| 161 | +for try await element in zippedAsyncSequence { |
| 162 | + print(element) // will print -> (1, "5") (2, "4") (3, "3") (4, "2") (5, "1") |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +### Zip3 |
| 167 | + |
| 168 | +`Zip3` is an AsyncSequence that combines the latest elements from two sequences according to their temporality and emits a tuple to the client. |
| 169 | +If any async sequence ends successfully or fails with an error, so too does the zipped async Sequence. |
| 170 | + |
| 171 | +```swift |
| 172 | +let asyncSequence1 = AsyncSequences.From([1, 2, 3, 4, 5]) |
| 173 | +let asyncSequence2 = AsyncSequences.From(["5", "4", "3", "2", "1"]) |
| 174 | +let asyncSequence3 = AsyncSequences.From([true, false, true, false, true]) |
| 175 | + |
| 176 | +let zippedAsyncSequence = AsyncSequences.Zip3(asyncSequence1, asyncSequence2, asyncSequence3) |
| 177 | + |
| 178 | +for try await element in zippedAsyncSequence { |
| 179 | + print(element) // will print -> (1, "5", true) (2, "4", false) (3, "3", true) (4, "2", false) (5, "1", true) |
| 180 | +} |
| 181 | +``` |
| 182 | + |
| 183 | +### Zip |
| 184 | + |
| 185 | +`Zip` is an AsyncSequence that combines the latest elements from several sequences according to their temporality and emits an array to the client. |
| 186 | +If any async sequence ends successfully or fails with an error, so too does the zipped async Sequence. |
| 187 | + |
| 188 | +```swift |
| 189 | +let asyncSequence1 = AsyncSequences.From([1, 2, 3]) |
| 190 | +let asyncSequence2 = AsyncSequences.From([1, 2, 3]) |
| 191 | +let asyncSequence3 = AsyncSequences.From([1, 2, 3]) |
| 192 | +let asyncSequence4 = AsyncSequences.From([1, 2, 3]) |
| 193 | +let asyncSequence5 = AsyncSequences.From([1, 2, 3]) |
| 194 | + |
| 195 | +let zippedAsyncSequence = AsyncSequences.Zip(asyncSequence1, asyncSequence2, asyncSequence3, asyncSequence4, asyncSequence5) |
| 196 | + |
| 197 | +for try await element in zippedAsyncSequence { |
| 198 | + print(element) // will print -> [1, 1, 1, 1, 1] [2, 2, 2, 2, 2] [3, 3, 3, 3, 3] |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +## Async Streams |
| 203 | + |
| 204 | +### Passthrough |
| 205 | + |
| 206 | +`Passthrough` is an async sequence in which one can send values over time. |
| 207 | + |
| 208 | +```swift |
| 209 | +let passthrough = AsyncStreams.Passthrough<Int>() |
| 210 | + |
| 211 | +Task { |
| 212 | + for try await element in passthrough { |
| 213 | + print(element) // will print 1 2 |
| 214 | + } |
| 215 | +} |
| 216 | + |
| 217 | +Task { |
| 218 | + for try await element in passthrough { |
| 219 | + print(element) // will print 1 2 |
| 220 | + } |
| 221 | +} |
| 222 | + |
| 223 | +.. later in the application flow |
| 224 | + |
| 225 | +passthrough.send(1) |
| 226 | +passthrough.send(2) |
| 227 | +``` |
| 228 | + |
| 229 | +### CurrentValue |
| 230 | + |
| 231 | +`CurrentValue` is an async sequence in which one can send values over time. |
| 232 | +The current value is always accessible as an instance variable. |
| 233 | +The current value is replayed for any new async loop. |
| 234 | + |
| 235 | +```swift |
| 236 | +let currentValue = AsyncStreams.CurrentValue<Int>(1) |
| 237 | + |
| 238 | +Task { |
| 239 | + for try await element in passthrough { |
| 240 | + print(element) // will print 1 2 |
| 241 | + } |
| 242 | +} |
| 243 | + |
| 244 | +Task { |
| 245 | + for try await element in passthrough { |
| 246 | + print(element) // will print 1 2 |
| 247 | + } |
| 248 | +} |
| 249 | + |
| 250 | +.. later in the application flow |
| 251 | + |
| 252 | +currentValue.send(2) |
| 253 | + |
| 254 | +print(currentValue.element) // will print 2 |
| 255 | +``` |
| 256 | + |
| 257 | +### Replay |
| 258 | + |
| 259 | +`Replay`is an async sequence in which one can send values over time. |
| 260 | +Values are buffered in a FIFO fashion so they can be iterated over by new loops. |
| 261 | +When the `bufferSize` is outreached the oldest value is dropped. |
| 262 | + |
| 263 | +```swift |
| 264 | +let replay = AsyncStreams.Replay<Int>(bufferSize: 3) |
| 265 | + |
| 266 | +(1...5).forEach { replay.send($0) } |
| 267 | + |
| 268 | +for try await element in replay { |
| 269 | + print(element) // will print 3, 4, 5 |
| 270 | +} |
| 271 | +``` |
| 272 | + |
| 273 | +## Operators |
| 274 | + |
| 275 | +### Collect |
| 276 | + |
| 277 | +`collect(_:)` iterates over each element of the AsyncSequence and give it to the async block. |
| 278 | + |
| 279 | +```swift |
| 280 | +let fromSequence = AsyncSequences.From([1, 2, 3]) |
| 281 | +fromSequence |
| 282 | + .collect { print($0) } // will print 1 2 3 |
| 283 | +``` |
| 284 | + |
| 285 | +### Scan |
| 286 | + |
| 287 | +`scan(_:_:)` transforms elements from the upstream async sequence by providing the current element to a closure along with the last value returned by the closure. Each intermediate value will be emitted in the downstream async sequence. |
| 288 | + |
| 289 | +```swift |
| 290 | +let sourceSequence = AsyncSequences.From([1, 2, 3, 4, 5]) |
| 291 | +let scannedSequence = sourceSequence.scan("") { accumulator, element in accumulator + "\(element)"} |
| 292 | + |
| 293 | +for try await element in scannedSequence { |
| 294 | + print(element) |
| 295 | +} |
| 296 | + |
| 297 | +// will print: |
| 298 | +1 |
| 299 | +12 |
| 300 | +123 |
| 301 | +1234 |
| 302 | +12345 |
| 303 | +``` |
| 304 | + |
| 305 | +### SwitchToLatest |
| 306 | + |
| 307 | +`switchToLatest()` re-emits elements sent by the most recently received async sequence. This operator applies only in the case where the upstream async sequence's `Element` is it-self an async sequence. |
| 308 | + |
| 309 | +``` |
| 310 | +let sourceSequence = AsyncSequences.From([1, 2, 3]) |
| 311 | +let mappedSequence = sourceSequence.map { element in |
| 312 | + AsyncSequences.From(["a\(element)", "b\(element)"]) |
| 313 | +} |
| 314 | +let switchedSequence = mappedSequence.switchToLatest() |
| 315 | +
|
| 316 | +for try await element in switchedSequence { |
| 317 | + print(element) // will print a3 b3 |
| 318 | +} |
| 319 | +``` |
| 320 | + |
| 321 | +### FlatMapLatest |
| 322 | + |
| 323 | +`flatMapLatest(_:)` transforms the upstream async sequence elements into an async sequence and flattens the sequence of events from these multiple sources async sequences to appear as if they were coming from a single async sequence of events. Mapping to a new async sequence will cancel the task related to the previous one. |
| 324 | + |
| 325 | +This operator is basically a shortcut for `map()` and `switchToLatest()`. |
| 326 | + |
| 327 | +```swift |
| 328 | +let sourceSequence = AsyncSequences.From([1, 2, 3]) |
| 329 | +let flatMapLatestSequence = sourceSequence.flatMapLatest { element in |
| 330 | + AsyncSequences.From(["a\(element)", "b\(element)"]) |
| 331 | +} |
| 332 | + |
| 333 | +for try await element in flatMapLatestSequence { |
| 334 | + print(element) // will print a3 b3 |
| 335 | +} |
| 336 | +``` |
| 337 | + |
| 338 | +### Prepend |
| 339 | + |
| 340 | +`prepend(_:)` prepends an element to the upstream async sequence. |
| 341 | + |
| 342 | +```swift |
| 343 | +let sourceSequence = AsyncSequences.From([1, 2, 3]) |
| 344 | +let prependSequence = sourceSequence.prepend(0) |
| 345 | + |
| 346 | +for try await element in prependSequence { |
| 347 | + print(element) // will print 0 1 2 3 |
| 348 | +} |
| 349 | +``` |
| 350 | + |
| 351 | +### HandleEvents |
| 352 | + |
| 353 | +`handleEvents(onStart:onElement:onCancel:onFinish)` performs the specified closures when async sequences events occur. |
| 354 | + |
| 355 | +```swift |
| 356 | +let sourceSequence = AsyncSequences.From([1, 2, 3, 4, 5]) |
| 357 | +let handledSequence = sourceSequence.handleEvents { |
| 358 | + print("Begin iterating") |
| 359 | +} onElement: { element in |
| 360 | + print("Element is \(element)") |
| 361 | +} onCancel: { |
| 362 | + print("Cancelled") |
| 363 | +} onFinish: { termination in |
| 364 | + print(termination) |
| 365 | +} |
| 366 | + |
| 367 | +for try await element in handledSequence {} |
| 368 | + |
| 369 | +// will print: |
| 370 | +// Begin iterating |
| 371 | +// Element is 1 |
| 372 | +// Element is 2 |
| 373 | +// Element is 3 |
| 374 | +// Element is 4 |
| 375 | +// Element is 5 |
| 376 | +// finished |
| 377 | +``` |
| 378 | + |
| 379 | +### EraseToAnyAsyncSequence |
| 380 | + |
| 381 | +`eraseToAnyAsyncSequence()` type-erases the async sequence into an AnyAsyncSequence. |
0 commit comments