diff --git a/.gitignore b/.gitignore index bb460e7..59e2947 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ xcuserdata/ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index e378654..0000000 --- a/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections.git", - "state": { - "branch": null, - "revision": "f504716c27d2e5d4144fa4794b12129301d17729", - "version": "1.0.3" - } - } - ] - }, - "version": 1 -} diff --git a/Package.swift b/Package.swift index 4f5d569..04601d7 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -16,23 +16,31 @@ let package = Package( name: "AsyncExtensions", targets: ["AsyncExtensions"]), ], - dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.3"))], + dependencies: [ + .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.3")), + .package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.14.0"), + .package(url: "https://github.com/apple/swift-async-algorithms.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.2.0")), + ], targets: [ .target( name: "AsyncExtensions", - dependencies: [.product(name: "Collections", package: "swift-collections")], - path: "Sources" -// , -// swiftSettings: [ -// .unsafeFlags([ -// "-Xfrontend", "-warn-concurrency", -// "-Xfrontend", "-enable-actor-data-race-checks", -// ]) -// ] + dependencies: [ + .product(name: "Collections", package: "swift-collections"), + .product(name: "Atomics", package: "swift-atomics") + ], + path: "Sources", + swiftSettings: [.swiftLanguageMode(.v5)] ), .testTarget( name: "AsyncExtensionsTests", - dependencies: ["AsyncExtensions"], - path: "Tests"), + dependencies: [ + "AsyncExtensions", + .product(name: "OpenCombine", package: "OpenCombine", condition: .when(platforms: [.linux])), + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms") + ], + path: "Tests", + swiftSettings: [.swiftLanguageMode(.v5)] + ), ] ) diff --git a/README.md b/README.md index 30fe0ef..71a93d0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ **AsyncExtensions** provides a collection of operators that intends to ease the creation and combination of `AsyncSequences`. -**AsyncExtensions** can be seen as a companion to Apple [swift-async-algorithms](https://github.com/apple/swift-async-algorithms). For now there is an overlap between both libraries, but when **swift-async-algorithms** becomes stable the overlapping operators while be deprecated in **AsyncExtensions**. Nevertheless **AsyncExtensions** will continue to provide the operators that the community needs and are not provided by Apple. +**AsyncExtensions** can be seen as a companion to Apple [swift-async-algorithms](https://github.com/apple/swift-async-algorithms), which provides operators that the community needs and are not provided by Apple. ## Adding AsyncExtensions as a Dependency @@ -44,11 +44,6 @@ AsyncStream) * [AsyncThrowingReplaySubject](./Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift): Throwing subject with a shared output. Maintain an replays a buffered amount of values ### Combiners -* [`zip(_:_:)`](./Sources/Combiners/Zip/AsyncZip2Sequence.swift): Zips two `AsyncSequence` into an AsyncSequence of tuple of elements -* [`zip(_:_:_:)`](./Sources/Combiners/Zip/AsyncZip3Sequence.swift): Zips three `AsyncSequence` into an AsyncSequence of tuple of elements -* [`zip(_:)`](./Sources/Combiners/Zip/AsyncZipSequence.swift): Zips any async sequences into an array of elements -* [`merge(_:_:)`](./Sources/Combiners/Merge/AsyncMerge2Sequence.swift): Merges two `AsyncSequence` into an AsyncSequence of elements -* [`merge(_:_:_:)`](./Sources/Combiners/Merge/AsyncMerge3Sequence.swift): Merges three `AsyncSequence` into an AsyncSequence of elements * [`merge(_:)`](./Sources/Combiners/Merge/AsyncMergeSequence.swift): Merges any `AsyncSequence` into an AsyncSequence of elements * [`withLatest(_:)`](./Sources/Combiners/WithLatestFrom/AsyncWithLatestFromSequence.swift): Combines elements from self with the last known element from an other `AsyncSequence` * [`withLatest(_:_:)`](./Sources/Combiners/WithLatestFrom/AsyncWithLatestFrom2Sequence.swift): Combines elements from self with the last known elements from two other async sequences @@ -58,7 +53,6 @@ AsyncStream) * [AsyncFailSequence](./Sources/Creators/AsyncFailSequence.swift): Creates an `AsyncSequence` that immediately fails * [AsyncJustSequence](./Sources/Creators/AsyncJustSequence.swift): Creates an `AsyncSequence` that emits an element an finishes * [AsyncThrowingJustSequence](./Sources/Creators/AsyncThrowingJustSequence.swift): Creates an `AsyncSequence` that emits an elements and finishes bases on a throwing closure -* [AsyncLazySequence](./Sources/Creators/AsyncLazySequence.swift): Creates an `AsyncSequence` of the elements from the base sequence * [AsyncTimerSequence](./Sources/Creators/AsyncTimerSequence.swift): Creates an `AsyncSequence` that emits a date value periodically * [AsyncStream Pipe](./Sources/Creators/AsyncStream+Pipe.swift): Creates an AsyncStream and returns a tuple standing for its inputs and outputs diff --git a/Sources/AsyncChannels/AsyncBufferedChannel.swift b/Sources/AsyncChannels/AsyncBufferedChannel.swift index 8c97127..c25460d 100644 --- a/Sources/AsyncChannels/AsyncBufferedChannel.swift +++ b/Sources/AsyncChannels/AsyncBufferedChannel.swift @@ -5,6 +5,7 @@ // Created by Thibault Wittemberg on 07/01/2022. // +import Atomics import DequeModule import OrderedCollections @@ -69,7 +70,7 @@ public final class AsyncBufferedChannel: AsyncSequence, Sendable { enum State: @unchecked Sendable { case idle case queued(Deque) - case awaiting(OrderedSet) + case awaiting([Awaiting]) case finished static var initial: State { @@ -77,19 +78,16 @@ public final class AsyncBufferedChannel: AsyncSequence, Sendable { } } - let ids: ManagedCriticalState + let ids: ManagedAtomic let state: ManagedCriticalState public init() { - self.ids = ManagedCriticalState(0) + self.ids = ManagedAtomic(0) self.state = ManagedCriticalState(.initial) } func generateId() -> Int { - self.ids.withCriticalRegion { ids in - ids += 1 - return ids - } + ids.wrappingIncrementThenLoad(by: 1, ordering: .relaxed) } var hasBufferedElements: Bool { @@ -155,32 +153,12 @@ public final class AsyncBufferedChannel: AsyncSequence, Sendable { func next(onSuspend: (() -> Void)? = nil) async -> Element? { let awaitingId = self.generateId() - let cancellation = ManagedCriticalState(false) + let cancellation = ManagedAtomic(false) - return await withTaskCancellationHandler { [state] in - let awaiting = state.withCriticalRegion { state -> Awaiting? in - cancellation.withCriticalRegion { cancellation in - cancellation = true - } - switch state { - case .awaiting(var awaitings): - let awaiting = awaitings.remove(.placeHolder(id: awaitingId)) - if awaitings.isEmpty { - state = .idle - } else { - state = .awaiting(awaitings) - } - return awaiting - default: - return nil - } - } - - awaiting?.continuation?.resume(returning: nil) - } operation: { + return await withTaskCancellationHandler { await withUnsafeContinuation { [state] (continuation: UnsafeContinuation) in let decision = state.withCriticalRegion { state -> AwaitingDecision in - let isCancelled = cancellation.withCriticalRegion { $0 } + let isCancelled = cancellation.load(ordering: .acquiring) guard !isCancelled else { return .resume(nil) } switch state { @@ -204,7 +182,13 @@ public final class AsyncBufferedChannel: AsyncSequence, Sendable { return .suspend } case .awaiting(var awaitings): - awaitings.updateOrAppend(Awaiting(id: awaitingId, continuation: continuation)) + let awaiting = Awaiting(id: awaitingId, continuation: continuation) + + if let index = awaitings.firstIndex(where: { $0 == awaiting }) { + awaitings[index] = awaiting + } else { + awaitings.append(awaiting) + } state = .awaiting(awaitings) return .suspend case .finished: @@ -218,6 +202,27 @@ public final class AsyncBufferedChannel: AsyncSequence, Sendable { onSuspend?() } } + } onCancel: { [state] in + let awaiting = state.withCriticalRegion { state -> Awaiting? in + cancellation.store(true, ordering: .releasing) + switch state { + case .awaiting(var awaitings): + let index = awaitings.firstIndex(where: { $0 == .placeHolder(id: awaitingId) }) + guard let index else { return nil } + let awaiting = awaitings[index] + awaitings.remove(at: index) + if awaitings.isEmpty { + state = .idle + } else { + state = .awaiting(awaitings) + } + return awaiting + default: + return nil + } + } + + awaiting?.continuation?.resume(returning: nil) } } diff --git a/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift b/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift index f34c80e..b3579f6 100644 --- a/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift +++ b/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift @@ -5,6 +5,7 @@ // Created by Thibault Wittemberg on 07/01/2022. // +import Atomics import DequeModule import OrderedCollections @@ -80,7 +81,7 @@ public final class AsyncThrowingBufferedChannel: AsyncS enum State: @unchecked Sendable { case idle case queued(Deque) - case awaiting(OrderedSet) + case awaiting([Awaiting]) case terminated(Termination) static var initial: State { @@ -88,19 +89,16 @@ public final class AsyncThrowingBufferedChannel: AsyncS } } - let ids: ManagedCriticalState + let ids: ManagedAtomic let state: ManagedCriticalState public init() { - self.ids = ManagedCriticalState(0) + self.ids = ManagedAtomic(0) self.state = ManagedCriticalState(.initial) } func generateId() -> Int { - self.ids.withCriticalRegion { ids in - ids += 1 - return ids - } + ids.wrappingIncrementThenLoad(by: 1, ordering: .relaxed) } var hasBufferedElements: Bool { @@ -176,32 +174,12 @@ public final class AsyncThrowingBufferedChannel: AsyncS func next(onSuspend: (() -> Void)? = nil) async throws -> Element? { let awaitingId = self.generateId() - let cancellation = ManagedCriticalState(false) + let cancellation = ManagedAtomic(false) - return try await withTaskCancellationHandler { [state] in - let awaiting = state.withCriticalRegion { state -> Awaiting? in - cancellation.withCriticalRegion { cancellation in - cancellation = true - } - switch state { - case .awaiting(var awaitings): - let awaiting = awaitings.remove(.placeHolder(id: awaitingId)) - if awaitings.isEmpty { - state = .idle - } else { - state = .awaiting(awaitings) - } - return awaiting - default: - return nil - } - } - - awaiting?.continuation?.resume(returning: nil) - } operation: { + return try await withTaskCancellationHandler { try await withUnsafeThrowingContinuation { [state] (continuation: UnsafeContinuation) in let decision = state.withCriticalRegion { state -> AwaitingDecision in - let isCancelled = cancellation.withCriticalRegion { $0 } + let isCancelled = cancellation.load(ordering: .acquiring) guard !isCancelled else { return .resume(nil) } switch state { @@ -228,7 +206,13 @@ public final class AsyncThrowingBufferedChannel: AsyncS return .suspend } case .awaiting(var awaitings): - awaitings.updateOrAppend(Awaiting(id: awaitingId, continuation: continuation)) + let awaiting = Awaiting(id: awaitingId, continuation: continuation) + + if let index = awaitings.firstIndex(where: { $0 == awaiting }) { + awaitings[index] = awaiting + } else { + awaitings.append(awaiting) + } state = .awaiting(awaitings) return .suspend case .terminated(.finished): @@ -245,6 +229,27 @@ public final class AsyncThrowingBufferedChannel: AsyncS onSuspend?() } } + } onCancel: { [state] in + let awaiting = state.withCriticalRegion { state -> Awaiting? in + cancellation.store(true, ordering: .releasing) + switch state { + case .awaiting(var awaitings): + let index = awaitings.firstIndex(where: { $0 == .placeHolder(id: awaitingId) }) + guard let index else { return nil } + let awaiting = awaitings[index] + awaitings.remove(at: index) + if awaitings.isEmpty { + state = .idle + } else { + state = .awaiting(awaitings) + } + return awaiting + default: + return nil + } + } + + awaiting?.continuation?.resume(returning: nil) } } diff --git a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift index 5225105..644f118 100644 --- a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift @@ -67,54 +67,57 @@ public final class AsyncCurrentValueSubject: AsyncSubject where Element /// Sends a value to all consumers /// - Parameter element: the value to send public func send(_ element: Element) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncBufferedChannel] in state.current = element - for channel in state.channels.values { - channel.send(element) - } + return Array(state.channels.values) + } + + for channel in channels { + channel.send(element) } } /// Finishes the async sequences with a normal ending. /// - Parameter termination: The termination to finish the subject. public func send(_ termination: Termination) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncBufferedChannel] in state.terminalState = termination let channels = Array(state.channels.values) state.channels.removeAll() - for channel in channels { - channel.finish() - } + return channels + } + + for channel in channels { + channel.finish() } } func handleNewConsumer() -> (iterator: AsyncBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncBufferedChannel() + var consumerId: Int! + var unregister: (@Sendable () -> Void)? - let (terminalState, current) = self.state.withCriticalRegion { state -> (Termination?, Element) in - (state.terminalState, state.current) - } - - if let terminalState = terminalState, terminalState.isFinished { - asyncBufferedChannel.finish() - return (asyncBufferedChannel.makeAsyncIterator(), {}) - } - - asyncBufferedChannel.send(current) - - let consumerId = self.state.withCriticalRegion { state -> Int in - state.ids += 1 - state.channels[state.ids] = asyncBufferedChannel - return state.ids + self.state.withCriticalRegion { state in + let terminalState = state.terminalState + if let terminalState, terminalState.isFinished { + asyncBufferedChannel.finish() + } else { + state.ids &+= 1 + consumerId = state.ids + state.channels[consumerId] = asyncBufferedChannel + asyncBufferedChannel.send(state.current) + } } - let unregister = { @Sendable [state] in - state.withCriticalRegion { state in - state.channels[consumerId] = nil + if let consumerId { + unregister = { @Sendable [state, consumerId] in + state.withCriticalRegion { state in + state.channels[consumerId] = nil + } } } - return (asyncBufferedChannel.makeAsyncIterator(), unregister) + return (asyncBufferedChannel.makeAsyncIterator(), unregister ?? {}) } public func makeAsyncIterator() -> AsyncIterator { diff --git a/Sources/AsyncSubjects/AsyncPassthroughSubject.swift b/Sources/AsyncSubjects/AsyncPassthroughSubject.swift index 61690f1..5fdded4 100644 --- a/Sources/AsyncSubjects/AsyncPassthroughSubject.swift +++ b/Sources/AsyncSubjects/AsyncPassthroughSubject.swift @@ -52,23 +52,27 @@ public final class AsyncPassthroughSubject: AsyncSubject { /// Sends a value to all consumers /// - Parameter element: the value to send public func send(_ element: Element) { - self.state.withCriticalRegion { state in - for channel in state.channels.values { - channel.send(element) - } + let channels = self.state.withCriticalRegion { state in + state.channels.values + } + + for channel in channels { + channel.send(element) } } /// Finishes the subject with a normal ending. /// - Parameter termination: The termination to finish the subject public func send(_ termination: Termination) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncBufferedChannel] in state.terminalState = termination let channels = Array(state.channels.values) state.channels.removeAll() - for channel in channels { - channel.finish() - } + return channels + } + + for channel in channels { + channel.finish() } } diff --git a/Sources/AsyncSubjects/AsyncReplaySubject.swift b/Sources/AsyncSubjects/AsyncReplaySubject.swift index f4e610e..64d424a 100644 --- a/Sources/AsyncSubjects/AsyncReplaySubject.swift +++ b/Sources/AsyncSubjects/AsyncReplaySubject.swift @@ -46,36 +46,40 @@ public final class AsyncReplaySubject: AsyncSubject where Element: Send /// Sends a value to all consumers /// - Parameter element: the value to send public func send(_ element: Element) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncBufferedChannel] in if state.buffer.count >= state.bufferSize && !state.buffer.isEmpty { state.buffer.removeFirst() } state.buffer.append(element) - for channel in state.channels.values { - channel.send(element) - } + return Array(state.channels.values) + } + + for channel in channels { + channel.send(element) } } /// Finishes the subject with a normal ending. /// - Parameter termination: The termination to finish the subject. public func send(_ termination: Termination) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncBufferedChannel] in state.terminalState = termination let channels = Array(state.channels.values) state.channels.removeAll() state.buffer.removeAll() state.bufferSize = 0 - for channel in channels { - channel.finish() - } + return channels + } + + for channel in channels { + channel.finish() } } func handleNewConsumer() -> (iterator: AsyncBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncBufferedChannel() - let (terminalState, elements) = self.state.withCriticalRegion { state -> (Termination?, [Element]) in + let (terminalState, elements) = self.state.withCriticalRegion { state in (state.terminalState, state.buffer) } diff --git a/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift index 2294b09..804a34a 100644 --- a/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift @@ -67,28 +67,32 @@ public final class AsyncThrowingCurrentValueSubject: As /// Sends a value to all consumers /// - Parameter element: the value to send public func send(_ element: Element) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncThrowingBufferedChannel] in state.current = element - for channel in state.channels.values { - channel.send(element) - } + return Array(state.channels.values) + } + + for channel in channels { + channel.send(element) } } /// Finishes the subject with either a normal ending or an error. /// - Parameter termination: The termination to finish the subject. public func send(_ termination: Termination) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncThrowingBufferedChannel] in state.terminalState = termination let channels = Array(state.channels.values) state.channels.removeAll() - for channel in channels { - switch termination { - case .finished: - channel.finish() - case .failure(let error): - channel.fail(error) - } + return channels + } + + for channel in channels { + switch termination { + case .finished: + channel.finish() + case .failure(let error): + channel.fail(error) } } } @@ -97,8 +101,8 @@ public final class AsyncThrowingCurrentValueSubject: As ) -> (iterator: AsyncThrowingBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncThrowingBufferedChannel() - let (terminalState, current) = self.state.withCriticalRegion { state -> (Termination?, Element) in - (state.terminalState, state.current) + let terminalState = self.state.withCriticalRegion { state -> Termination? in + state.terminalState } if let terminalState = terminalState { @@ -111,11 +115,10 @@ public final class AsyncThrowingCurrentValueSubject: As return (asyncBufferedChannel.makeAsyncIterator(), {}) } - asyncBufferedChannel.send(current) - let consumerId = self.state.withCriticalRegion { state -> Int in state.ids += 1 state.channels[state.ids] = asyncBufferedChannel + asyncBufferedChannel.send(state.current) return state.ids } diff --git a/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift b/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift index c1da4a5..374c091 100644 --- a/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift @@ -53,28 +53,31 @@ public final class AsyncThrowingPassthroughSubject: Asy /// Sends a value to all consumers /// - Parameter element: the value to send public func send(_ element: Element) { - self.state.withCriticalRegion { state in - for channel in state.channels.values { - channel.send(element) - } + let channels = self.state.withCriticalRegion { state in + state.channels.values + } + + for channel in channels { + channel.send(element) } } /// Finishes the subject with either a normal ending or an error. /// - Parameter termination: The termination to finish the subject public func send(_ termination: Termination) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncThrowingBufferedChannel] in state.terminalState = termination let channels = Array(state.channels.values) state.channels.removeAll() + return channels + } - for channel in channels { - switch termination { - case .finished: - channel.finish() - case .failure(let error): - channel.fail(error) - } + for channel in channels { + switch termination { + case .finished: + channel.finish() + case .failure(let error): + channel.fail(error) } } } diff --git a/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift b/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift index c736d49..36dc5e9 100644 --- a/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift @@ -45,33 +45,37 @@ public final class AsyncThrowingReplaySubject: AsyncSub /// Sends a value to all consumers /// - Parameter element: the value to send public func send(_ element: Element) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncThrowingBufferedChannel] in if state.buffer.count >= state.bufferSize && !state.buffer.isEmpty { state.buffer.removeFirst() } state.buffer.append(element) - for channel in state.channels.values { - channel.send(element) - } + return Array(state.channels.values) + } + + for channel in channels { + channel.send(element) } } /// Finishes the subject with either a normal ending or an error. /// - Parameter termination: The termination to finish the subject public func send(_ termination: Termination) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncThrowingBufferedChannel] in state.terminalState = termination let channels = Array(state.channels.values) state.channels.removeAll() state.buffer.removeAll() state.bufferSize = 0 - for channel in channels { - switch termination { - case .finished: - channel.finish() - case .failure(let error): - channel.fail(error) - } + return channels + } + + for channel in channels { + switch termination { + case .finished: + channel.finish() + case .failure(let error): + channel.fail(error) } } } @@ -80,7 +84,7 @@ public final class AsyncThrowingReplaySubject: AsyncSub ) -> (iterator: AsyncThrowingBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncThrowingBufferedChannel() - let (terminalState, elements) = self.state.withCriticalRegion { state -> (Termination?, [Element]) in + let (terminalState, elements) = self.state.withCriticalRegion { state in (state.terminalState, state.buffer) } diff --git a/Sources/Combiners/Merge/AsyncMerge2Sequence.swift b/Sources/Combiners/Merge/AsyncMerge2Sequence.swift deleted file mode 100644 index 89a4b23..0000000 --- a/Sources/Combiners/Merge/AsyncMerge2Sequence.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// AsyncMerge2Sequence.swift -// -// -// Created by Thibault Wittemberg on 31/03/2022. -// - -/// Creates an asynchronous sequence of elements from two underlying asynchronous sequences -public func merge( - _ base1: Base1, - _ base2: Base2 -) -> AsyncMerge2Sequence { - AsyncMerge2Sequence(base1, base2) -} - -/// An asynchronous sequence of elements from two underlying asynchronous sequences -/// -/// In a `AsyncMerge2Sequence` instance, the *i*th element is the *i*th element -/// resolved in sequential order out of the two underlying asynchronous sequences. -/// Use the `merge(_:_:)` function to create an `AsyncMerge2Sequence`. -public struct AsyncMerge2Sequence: AsyncSequence -where Base1.Element == Base2.Element { - public typealias Element = Base1.Element - public typealias AsyncIterator = Iterator - - let base1: Base1 - let base2: Base2 - - public init(_ base1: Base1, _ base2: Base2) { - self.base1 = base1 - self.base2 = base2 - } - - public func makeAsyncIterator() -> Iterator { - Iterator( - base1: self.base1, - base2: self.base2 - ) - } - - public struct Iterator: AsyncIteratorProtocol { - let mergeStateMachine: MergeStateMachine - - init(base1: Base1, base2: Base2) { - self.mergeStateMachine = MergeStateMachine( - base1, - base2 - ) - } - - public mutating func next() async rethrows -> Element? { - let mergedElement = await self.mergeStateMachine.next() - switch mergedElement { - case .element(let result): - return try result._rethrowGet() - case .termination: - return nil - } - } - } -} - -extension AsyncMerge2Sequence: Sendable where Base1: Sendable, Base2: Sendable {} diff --git a/Sources/Combiners/Merge/AsyncMerge3Sequence.swift b/Sources/Combiners/Merge/AsyncMerge3Sequence.swift deleted file mode 100644 index 468b1ee..0000000 --- a/Sources/Combiners/Merge/AsyncMerge3Sequence.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// AsyncMerge3Sequence.swift -// -// -// Created by Thibault Wittemberg on 31/03/2022. -// - -/// Creates an asynchronous sequence of elements from three underlying asynchronous sequences -public func merge( - _ base1: Base1, - _ base2: Base2, - _ base3: Base3 -) -> AsyncMerge3Sequence { - AsyncMerge3Sequence(base1, base2, base3) -} - -/// An asynchronous sequence of elements from three underlying asynchronous sequences -/// -/// In a `AsyncMerge3Sequence` instance, the *i*th element is the *i*th element -/// resolved in sequential order out of the two underlying asynchronous sequences. -/// Use the `merge(_:_:_:)` function to create an `AsyncMerge3Sequence`. -public struct AsyncMerge3Sequence: AsyncSequence -where Base1.Element == Base2.Element, Base3.Element == Base2.Element { - public typealias Element = Base1.Element - public typealias AsyncIterator = Iterator - - let base1: Base1 - let base2: Base2 - let base3: Base3 - - public init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { - self.base1 = base1 - self.base2 = base2 - self.base3 = base3 - } - - public func makeAsyncIterator() -> Iterator { - Iterator( - base1: self.base1, - base2: self.base2, - base3: self.base3 - ) - } - - public struct Iterator: AsyncIteratorProtocol { - let mergeStateMachine: MergeStateMachine - - init(base1: Base1, base2: Base2, base3: Base3) { - self.mergeStateMachine = MergeStateMachine( - base1, - base2, - base3 - ) - } - - public mutating func next() async rethrows -> Element? { - let mergedElement = await self.mergeStateMachine.next() - switch mergedElement { - case .element(let result): - return try result._rethrowGet() - case .termination: - return nil - } - } - } -} - -extension AsyncMerge3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable {} -extension AsyncMerge3Sequence.Iterator: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable {} diff --git a/Sources/Combiners/Merge/MergeStateMachine.swift b/Sources/Combiners/Merge/MergeStateMachine.swift index 3cbec30..3affdc6 100644 --- a/Sources/Combiners/Merge/MergeStateMachine.swift +++ b/Sources/Combiners/Merge/MergeStateMachine.swift @@ -196,8 +196,6 @@ struct MergeStateMachine: Sendable { func next() async -> RegulatedElement { await withTaskCancellationHandler { - self.unsuspendAndClearOnCancel() - } operation: { self.requestNextRegulatedElements() let regulatedElement = await withUnsafeContinuation { (continuation: UnsafeContinuation, Never>) in @@ -244,6 +242,8 @@ struct MergeStateMachine: Sendable { } return regulatedElement + } onCancel: { + self.unsuspendAndClearOnCancel() } } } diff --git a/Sources/Combiners/WithLatestFrom/AsyncWithLatestFrom2Sequence.swift b/Sources/Combiners/WithLatestFrom/AsyncWithLatestFrom2Sequence.swift index d10fe9f..b5007df 100644 --- a/Sources/Combiners/WithLatestFrom/AsyncWithLatestFrom2Sequence.swift +++ b/Sources/Combiners/WithLatestFrom/AsyncWithLatestFrom2Sequence.swift @@ -172,12 +172,7 @@ where Other1: Sendable, Other2: Sendable, Other1.Element: Sendable, Other2.Eleme let shouldReturnNil = self.isTerminated.withCriticalRegion { $0 } guard !shouldReturnNil else { return nil } - return try await withTaskCancellationHandler { [isTerminated, othersTask] in - isTerminated.withCriticalRegion { isTerminated in - isTerminated = true - } - othersTask?.cancel() - } operation: { [othersTask, othersState, onBaseElement] in + return try await withTaskCancellationHandler { [othersTask, othersState, onBaseElement] in do { while true { guard let baseElement = try await self.base.next() else { @@ -219,6 +214,11 @@ where Other1: Sendable, Other2: Sendable, Other1.Element: Sendable, Other2.Eleme othersTask?.cancel() throw error } + } onCancel: { [isTerminated, othersTask] in + isTerminated.withCriticalRegion { isTerminated in + isTerminated = true + } + othersTask?.cancel() } } } diff --git a/Sources/Combiners/WithLatestFrom/AsyncWithLatestFromSequence.swift b/Sources/Combiners/WithLatestFrom/AsyncWithLatestFromSequence.swift index d02d44f..6481f85 100644 --- a/Sources/Combiners/WithLatestFrom/AsyncWithLatestFromSequence.swift +++ b/Sources/Combiners/WithLatestFrom/AsyncWithLatestFromSequence.swift @@ -121,9 +121,7 @@ where Other: Sendable, Other.Element: Sendable { public mutating func next() async rethrows -> Element? { guard !self.isTerminated else { return nil } - return try await withTaskCancellationHandler { [otherTask] in - otherTask?.cancel() - } operation: { [otherTask, otherState, onBaseElement] in + return try await withTaskCancellationHandler { [otherTask, otherState, onBaseElement] in do { while true { guard let baseElement = try await self.base.next() else { @@ -157,8 +155,10 @@ where Other: Sendable, Other.Element: Sendable { otherTask?.cancel() throw error } + } onCancel: { [otherTask] in + otherTask?.cancel() } - } + } } } diff --git a/Sources/Combiners/Zip/AsyncZip2Sequence.swift b/Sources/Combiners/Zip/AsyncZip2Sequence.swift deleted file mode 100644 index d9ebe93..0000000 --- a/Sources/Combiners/Zip/AsyncZip2Sequence.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// AsyncZip2Sequence.swift -// -// -// Created by Thibault Wittemberg on 13/01/2022. -// - -/// `zip` produces an `AsyncSequence` that combines the latest elements from two sequences according to their temporality -/// and emits a tuple to the client. If any Async Sequence ends successfully or fails with an error, so to does the zipped -/// Async Sequence. -/// -/// ``` -/// let asyncSequence1 = [1, 2, 3, 4, 5].async -/// let asyncSequence2 = ["1", "2", "3", "4", "5"].async -/// -/// let zippedAsyncSequence = zip(asyncSequence1, asyncSequence2) -/// -/// for await element in zippedAsyncSequence { -/// print(element) // will print -> (1, "1") (2, "2") (3, "3") (4, "4") (5, "5") -/// } -/// ``` -/// Use the `zip(_:_:)` function to create an `AsyncZip2Sequence`. -public func zip( - _ base1: Base1, - _ base2: Base2 -) -> AsyncZip2Sequence { - AsyncZip2Sequence(base1, base2) -} - -public struct AsyncZip2Sequence: AsyncSequence -where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable { - public typealias Element = (Base1.Element, Base2.Element) - public typealias AsyncIterator = Iterator - - let base1: Base1 - let base2: Base2 - - init(_ base1: Base1, _ base2: Base2) { - self.base1 = base1 - self.base2 = base2 - } - - public func makeAsyncIterator() -> AsyncIterator { - Iterator( - base1, - base2 - ) - } - - public struct Iterator: AsyncIteratorProtocol { - let runtime: Zip2Runtime - - init(_ base1: Base1, _ base2: Base2) { - self.runtime = Zip2Runtime(base1, base2) - } - - public func next() async rethrows -> Element? { - try await self.runtime.next() - } - } -} diff --git a/Sources/Combiners/Zip/AsyncZip3Sequence.swift b/Sources/Combiners/Zip/AsyncZip3Sequence.swift deleted file mode 100644 index 89d8a24..0000000 --- a/Sources/Combiners/Zip/AsyncZip3Sequence.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// AsyncZip3Sequence.swift -// -// -// Created by Thibault Wittemberg on 24/09/2022. -// - -/// `zip` produces an `AsyncSequence` that combines the latest elements from three sequences according to their temporality -/// and emits a tuple to the client. If any Async Sequence ends successfully or fails with an error, so to does the zipped -/// Async Sequence. -/// -/// ``` -/// let asyncSequence1 = [1, 2, 3, 4, 5].async -/// let asyncSequence2 = ["1", "2", "3", "4", "5"].async -/// let asyncSequence3 = ["A", "B", "C", "D", "E"].async -/// -/// let zippedAsyncSequence = zip(asyncSequence1, asyncSequence2, asyncSequence3) -/// -/// for await element in zippedAsyncSequence { -/// print(element) // will print -> (1, "1", "A") (2, "2", "B") (3, "3", "V") (4, "4", "D") (5, "5", "E") -/// } -/// ``` -/// Use the `zip(_:_:_:)` function to create an `AsyncZip3Sequence`. -public func zip( - _ base1: Base1, - _ base2: Base2, - _ base3: Base3 -) -> AsyncZip3Sequence { - AsyncZip3Sequence(base1, base2, base3) -} - -public struct AsyncZip3Sequence: AsyncSequence -where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable, Base3: Sendable, Base3.Element: Sendable { - public typealias Element = (Base1.Element, Base2.Element, Base3.Element) - public typealias AsyncIterator = Iterator - - let base1: Base1 - let base2: Base2 - let base3: Base3 - - init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { - self.base1 = base1 - self.base2 = base2 - self.base3 = base3 - } - - public func makeAsyncIterator() -> AsyncIterator { - Iterator( - base1, - base2, - base3 - ) - } - - public struct Iterator: AsyncIteratorProtocol { - let runtime: Zip3Runtime - - init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { - self.runtime = Zip3Runtime(base1, base2, base3) - } - - public func next() async rethrows -> Element? { - try await self.runtime.next() - } - } -} diff --git a/Sources/Combiners/Zip/AsyncZipSequence.swift b/Sources/Combiners/Zip/AsyncZipSequence.swift deleted file mode 100644 index 74b0b01..0000000 --- a/Sources/Combiners/Zip/AsyncZipSequence.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// AsyncZipSequence.swift -// -// -// Created by Thibault Wittemberg on 24/09/2022. -// - -/// `zip` produces an `AsyncSequence` that combines the latest elements from sequences according to their temporality -/// and emits an array to the client. If any Async Sequence ends successfully or fails with an error, so to does the zipped -/// Async Sequence. -/// -/// ``` -/// let asyncSequence1 = [1, 2, 3, 4, 5].async -/// let asyncSequence2 = [1, 2, 3, 4, 5].async -/// let asyncSequence3 = [1, 2, 3, 4, 5].async -/// let asyncSequence4 = [1, 2, 3, 4, 5].async -/// let asyncSequence5 = [1, 2, 3, 4, 5].async -/// -/// let zippedAsyncSequence = zip(asyncSequence1, asyncSequence2, asyncSequence3, asyncSequence4, asyncSequence5) -/// -/// for await element in zippedAsyncSequence { -/// print(element) // will print -> [1, 1, 1, 1, 1] [2, 2, 2, 2, 2] [3, 3, 3, 3, 3] [4, 4, 4, 4, 4] [5, 5, 5, 5, 5] -/// } -/// ``` -/// Use the `zip(_:)` function to create an `AsyncZipSequence`. -public func zip(_ bases: Base...) -> AsyncZipSequence { - AsyncZipSequence(bases) -} - -public struct AsyncZipSequence: AsyncSequence -where Base: Sendable, Base.Element: Sendable { - public typealias Element = [Base.Element] - public typealias AsyncIterator = Iterator - - let bases: [Base] - - init(_ bases: [Base]) { - self.bases = bases - } - - public func makeAsyncIterator() -> AsyncIterator { - Iterator(bases) - } - - public struct Iterator: AsyncIteratorProtocol { - let runtime: ZipRuntime - - init(_ bases: [Base]) { - self.runtime = ZipRuntime(bases) - } - - public func next() async rethrows -> Element? { - try await self.runtime.next() - } - } -} diff --git a/Sources/Combiners/Zip/Zip2Runtime.swift b/Sources/Combiners/Zip/Zip2Runtime.swift deleted file mode 100644 index 699b8ca..0000000 --- a/Sources/Combiners/Zip/Zip2Runtime.swift +++ /dev/null @@ -1,214 +0,0 @@ -// -// Zip2Runtime.swift -// -// -// Created by Thibault Wittemberg on 24/09/2022. -// - -final class Zip2Runtime: Sendable -where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable { - typealias ZipStateMachine = Zip2StateMachine - - private let stateMachine = ManagedCriticalState(ZipStateMachine()) - - init(_ base1: Base1, _ base2: Base2) { - self.stateMachine.withCriticalRegion { machine in - machine.taskIsStarted(task: Task { - await withTaskGroup(of: Void.self) { group in - group.addTask { - var base1Iterator = base1.makeAsyncIterator() - - do { - while true { - await withUnsafeContinuation { (continuation: UnsafeContinuation) in - let output = self.stateMachine.withCriticalRegion { machine in - machine.newLoopFromBase1(suspendedBase: continuation) - } - - self.handle(newLoopFromBaseOutput: output) - } - - guard let element1 = try await base1Iterator.next() else { - break - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.base1HasProducedElement(element: element1) - } - - self.handle(baseHasProducedElementOutput: output) - } - } catch { - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseHasProducedFailure(error: error) - } - - self.handle(baseHasProducedFailureOutput: output) - } - - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.baseIsFinished() - } - - self.handle(baseIsFinishedOutput: output) - } - - group.addTask { - var base2Iterator = base2.makeAsyncIterator() - - do { - while true { - await withUnsafeContinuation { (continuation: UnsafeContinuation) in - let output = self.stateMachine.withCriticalRegion { machine in - machine.newLoopFromBase2(suspendedBase: continuation) - } - - self.handle(newLoopFromBaseOutput: output) - } - - guard let element2 = try await base2Iterator.next() else { - break - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.base2HasProducedElement(element: element2) - } - - self.handle(baseHasProducedElementOutput: output) - } - } catch { - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseHasProducedFailure(error: error) - } - - self.handle(baseHasProducedFailureOutput: output) - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseIsFinished() - } - - self.handle(baseIsFinishedOutput: output) - } - } - }) - } - } - - private func handle(newLoopFromBaseOutput: ZipStateMachine.NewLoopFromBaseOutput) { - switch newLoopFromBaseOutput { - case .none: - break - - case .resumeBases(let suspendedBases): - suspendedBases.forEach { $0.resume() } - - case .terminate(let task, let suspendedBase, let suspendedDemand): - suspendedBase.resume() - suspendedDemand?.resume(returning: nil) - task?.cancel() - } - } - - private func handle(baseHasProducedElementOutput: ZipStateMachine.BaseHasProducedElementOutput) { - switch baseHasProducedElementOutput { - case .none: - break - - case .resumeDemand(let suspendedDemand, let result1, let result2): - suspendedDemand?.resume(returning: (result1, result2)) - - case .terminate(let task, let suspendedBases): - suspendedBases?.forEach { $0.resume() } - task?.cancel() - } - } - - private func handle(baseHasProducedFailureOutput: ZipStateMachine.BaseHasProducedFailureOutput) { - switch baseHasProducedFailureOutput { - case .resumeDemandAndTerminate(let task, let suspendedDemand, let suspendedBases, let result1, let result2): - suspendedDemand?.resume(returning: (result1, result2)) - suspendedBases.forEach { $0.resume() } - task?.cancel() - - case .terminate(let task, let suspendedBases): - suspendedBases?.forEach { $0.resume() } - task?.cancel() - } - } - - private func handle(baseIsFinishedOutput: ZipStateMachine.BaseIsFinishedOutput) { - switch baseIsFinishedOutput { - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0?.resume(returning: nil) } - task?.cancel() - } - } - - func next() async rethrows -> (Base1.Element, Base2.Element)? { - try await withTaskCancellationHandler { - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.rootTaskIsCancelled() - } - - self.handle(rootTaskIsCancelledOutput: output) - } operation: { - let results = await withUnsafeContinuation { (continuation: UnsafeContinuation<(Result, Result)?, Never>) in - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.newDemandFromConsumer(suspendedDemand: continuation) - } - - self.handle(newDemandFromConsumerOutput: output) - } - - guard let results = results else { - return nil - } - - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.demandIsFulfilled() - } - - self.handle(demandIsFulfilledOutput: output) - - return try (results.0._rethrowGet(), results.1._rethrowGet()) - } - } - - private func handle(rootTaskIsCancelledOutput: ZipStateMachine.RootTaskIsCancelledOutput) { - switch rootTaskIsCancelledOutput { - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0?.resume(returning: nil) } - task?.cancel() - } - } - - private func handle(newDemandFromConsumerOutput: ZipStateMachine.NewDemandFromConsumerOutput) { - switch newDemandFromConsumerOutput { - case .none: - break - - case .resumeBases(let suspendedBases): - suspendedBases.forEach { $0.resume() } - - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0?.resume(returning: nil) } - task?.cancel() - } - } - - private func handle(demandIsFulfilledOutput: ZipStateMachine.DemandIsFulfilledOutput) { - switch demandIsFulfilledOutput { - case .none: - break - - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0.resume(returning: nil) } - task?.cancel() - } - } -} diff --git a/Sources/Combiners/Zip/Zip2StateMachine.swift b/Sources/Combiners/Zip/Zip2StateMachine.swift deleted file mode 100644 index 4b3b43e..0000000 --- a/Sources/Combiners/Zip/Zip2StateMachine.swift +++ /dev/null @@ -1,373 +0,0 @@ -// -// Zip2StateMachine.swift -// -// -// Created by Thibault Wittemberg on 24/09/2022. -// - -struct Zip2StateMachine: Sendable -where Element1: Sendable, Element2: Sendable { - private enum State { - case initial - case started(task: Task) - case awaitingDemandFromConsumer( - task: Task?, - suspendedBases: [UnsafeContinuation] - ) - case awaitingBaseResults( - task: Task?, - result1: Result?, - result2: Result?, - suspendedBases: [UnsafeContinuation], - suspendedDemand: UnsafeContinuation<(Result, Result)?, Never>? - ) - case finished - } - - private var state: State = .initial - - mutating func taskIsStarted(task: Task) { - switch self.state { - case .initial: - self.state = .started(task: task) - - default: - assertionFailure("Inconsistent state, the task cannot start while the state is other than initial") - } - } - - enum NewDemandFromConsumerOutput { - case none - case resumeBases(suspendedBases: [UnsafeContinuation]) - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [UnsafeContinuation<(Result, Result)?, Never>?]? - ) - } - - mutating func newDemandFromConsumer( - suspendedDemand: UnsafeContinuation<(Result, Result)?, Never> - ) -> NewDemandFromConsumerOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: [suspendedDemand]) - - case .started(let task): - self.state = .awaitingBaseResults(task: task, result1: nil, result2: nil, suspendedBases: [], suspendedDemand: suspendedDemand) - return .none - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .awaitingBaseResults(task: task, result1: nil, result2: nil, suspendedBases: [], suspendedDemand: suspendedDemand) - return .resumeBases(suspendedBases: suspendedBases) - - case .awaitingBaseResults(let task, _, _, let suspendedBases, let oldSuspendedDemand): - assertionFailure("Inconsistent state, a demand is already suspended") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [oldSuspendedDemand, suspendedDemand]) - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: [suspendedDemand]) - } - } - - enum NewLoopFromBaseOutput { - case none - case resumeBases(suspendedBases: [UnsafeContinuation]) - case terminate( - task: Task?, - suspendedBase: UnsafeContinuation, - suspendedDemand: UnsafeContinuation<(Result, Result)?, Never>? - ) - } - - mutating func newLoopFromBase1(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBase: suspendedBase, suspendedDemand: nil) - - case .started(let task): - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: [suspendedBase]) - return .none - - case .awaitingDemandFromConsumer(let task, var suspendedBases): - assert(suspendedBases.count < 2, "There cannot be more than 2 suspended base at the same time") - suspendedBases.append(suspendedBase) - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .awaitingBaseResults(let task, let result1, let result2, var suspendedBases, let suspendedDemand): - assert(suspendedBases.count < 2, "There cannot be more than 2 suspended bases at the same time") - if result1 != nil { - suspendedBases.append(suspendedBase) - self.state = .awaitingBaseResults(task: task, result1: result1, result2: result2, suspendedBases: suspendedBases, suspendedDemand: suspendedDemand) - return .none - } else { - self.state = .awaitingBaseResults(task: task, result1: result1, result2: result2, suspendedBases: suspendedBases, suspendedDemand: suspendedDemand) - return .resumeBases(suspendedBases: [suspendedBase]) - } - - case .finished: - return .terminate(task: nil, suspendedBase: suspendedBase, suspendedDemand: nil) - } - } - - mutating func newLoopFromBase2(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { - switch state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBase: suspendedBase, suspendedDemand: nil) - - case .started(let task): - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: [suspendedBase]) - return .none - - case .awaitingDemandFromConsumer(let task, var suspendedBases): - assert(suspendedBases.count < 2, "There cannot be more than 2 suspended base at the same time") - suspendedBases.append(suspendedBase) - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .awaitingBaseResults(let task, let result1, let result2, var suspendedBases, let suspendedDemand): - assert(suspendedBases.count < 2, "There cannot be more than 2 suspended bases at the same time") - if result2 != nil { - suspendedBases.append(suspendedBase) - self.state = .awaitingBaseResults(task: task, result1: result1, result2: result2, suspendedBases: suspendedBases, suspendedDemand: suspendedDemand) - return .none - } else { - self.state = .awaitingBaseResults(task: task, result1: result1, result2: result2, suspendedBases: suspendedBases, suspendedDemand: suspendedDemand) - return .resumeBases(suspendedBases: [suspendedBase]) - } - - case .finished: - return .terminate(task: nil, suspendedBase: suspendedBase, suspendedDemand: nil) - } - } - - enum BaseHasProducedElementOutput { - case none - case resumeDemand( - suspendedDemand: UnsafeContinuation<(Result, Result)?, Never>?, - result1: Result, - result2: Result - ) - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]? - ) - } - - mutating func base1HasProducedElement(element: Element1) -> BaseHasProducedElementOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil) - - case .started(let task): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases) - - case .awaitingBaseResults(let task, _, let result2, let suspendedBases, let suspendedDemand): - if let result2 = result2 { - self.state = .awaitingBaseResults(task: task, result1: .success(element), result2: result2, suspendedBases: suspendedBases, suspendedDemand: nil) - return .resumeDemand(suspendedDemand: suspendedDemand, result1: .success(element), result2: result2) - } else { - self.state = .awaitingBaseResults(task: task, result1: .success(element), result2: nil, suspendedBases: suspendedBases, suspendedDemand: suspendedDemand) - return .none - } - - case .finished: - return .terminate(task: nil, suspendedBases: nil) - } - } - - enum BaseHasProducedFailureOutput { - case resumeDemandAndTerminate( - task: Task?, - suspendedDemand: UnsafeContinuation<(Result, Result)?, Never>?, - suspendedBases: [UnsafeContinuation], - result1: Result, - result2: Result - ) - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]? - ) - } - - mutating func baseHasProducedFailure(error: any Error) -> BaseHasProducedFailureOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil) - - case .started(let task): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases) - - case .awaitingBaseResults(let task, _, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .resumeDemandAndTerminate( - task: task, - suspendedDemand: suspendedDemand, - suspendedBases: suspendedBases, - result1: .failure(error), - result2: .failure(error) - ) - - case .finished: - return .terminate(task: nil, suspendedBases: nil) - } - } - - mutating func base2HasProducedElement(element: Element2) -> BaseHasProducedElementOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil) - - case .started(let task): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases) - - case .awaitingBaseResults(let task, let result1, _, let suspendedBases, let suspendedDemand): - if let result1 = result1 { - self.state = .awaitingBaseResults(task: task, result1: result1, result2: .success(element), suspendedBases: suspendedBases, suspendedDemand: nil) - return .resumeDemand(suspendedDemand: suspendedDemand, result1: result1, result2: .success(element)) - } else { - self.state = .awaitingBaseResults(task: task, result1: nil, result2: .success(element), suspendedBases: suspendedBases, suspendedDemand: suspendedDemand) - return .none - } - - case .finished: - return .terminate(task: nil, suspendedBases: nil) - } - } - - enum DemandIsFulfilledOutput { - case none - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [UnsafeContinuation<(Result, Result)?, Never>]? - ) - } - - mutating func demandIsFulfilled() -> DemandIsFulfilledOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - - case .started(let task): - assertionFailure("Inconsistent state, results are not yet available to be acknowledged") - self.state = .finished - return .terminate(task: task, suspendedBases: nil, suspendedDemands: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - assertionFailure("Inconsistent state, results are not yet available to be acknowledged") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) - - case .awaitingBaseResults(let task, let result1, let result2, let suspendedBases, let suspendedDemand): - assert(suspendedDemand == nil, "Inconsistent state, there cannot be a suspended demand when ackowledging the demand") - assert(result1 != nil && result2 != nil, "Inconsistent state, all results are not yet available to be acknowledged") - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - } - } - - enum RootTaskIsCancelledOutput { - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [UnsafeContinuation<(Result, Result)?, Never>?]? - ) - } - - mutating func rootTaskIsCancelled() -> RootTaskIsCancelledOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - - case .started(let task): - self.state = .finished - return .terminate(task: task, suspendedBases: nil, suspendedDemands: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) - - case .awaitingBaseResults(let task, _, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - } - } - - enum BaseIsFinishedOutput { - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [UnsafeContinuation<(Result, Result)?, Never>?]? - ) - } - - mutating func baseIsFinished() -> BaseIsFinishedOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - - case .started(let task): - self.state = .finished - return .terminate(task: task, suspendedBases: nil, suspendedDemands: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) - - case .awaitingBaseResults(let task, _, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - } - } -} diff --git a/Sources/Combiners/Zip/Zip3Runtime.swift b/Sources/Combiners/Zip/Zip3Runtime.swift deleted file mode 100644 index eeb98f9..0000000 --- a/Sources/Combiners/Zip/Zip3Runtime.swift +++ /dev/null @@ -1,252 +0,0 @@ -// -// Zip3Runtime.swift -// -// -// Created by Thibault Wittemberg on 24/09/2022. -// - -final class Zip3Runtime: Sendable -where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable, Base3: Sendable, Base3.Element: Sendable { - typealias ZipStateMachine = Zip3StateMachine - - private let stateMachine = ManagedCriticalState(ZipStateMachine()) - - init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { - self.stateMachine.withCriticalRegion { machine in - machine.taskIsStarted(task: Task { - await withTaskGroup(of: Void.self) { group in - group.addTask { - var base1Iterator = base1.makeAsyncIterator() - - do { - while true { - await withUnsafeContinuation { (continuation: UnsafeContinuation) in - let output = self.stateMachine.withCriticalRegion { machine in - machine.newLoopFromBase1(suspendedBase: continuation) - } - - self.handle(newLoopFromBaseOutput: output) - } - - guard let element1 = try await base1Iterator.next() else { - break - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.base1HasProducedElement(element: element1) - } - - self.handle(baseHasProducedElementOutput: output) - } - } catch { - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseHasProducedFailure(error: error) - } - - self.handle(baseHasProducedFailureOutput: output) - } - - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.baseIsFinished() - } - - self.handle(baseIsFinishedOutput: output) - } - - group.addTask { - var base2Iterator = base2.makeAsyncIterator() - - do { - while true { - await withUnsafeContinuation { (continuation: UnsafeContinuation) in - let output = self.stateMachine.withCriticalRegion { machine in - machine.newLoopFromBase2(suspendedBase: continuation) - } - - self.handle(newLoopFromBaseOutput: output) - } - - guard let element2 = try await base2Iterator.next() else { - break - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.base2HasProducedElement(element: element2) - } - - self.handle(baseHasProducedElementOutput: output) - } - } catch { - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseHasProducedFailure(error: error) - } - - self.handle(baseHasProducedFailureOutput: output) - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseIsFinished() - } - - self.handle(baseIsFinishedOutput: output) - } - - group.addTask { - var base3Iterator = base3.makeAsyncIterator() - - do { - while true { - await withUnsafeContinuation { (continuation: UnsafeContinuation) in - let output = self.stateMachine.withCriticalRegion { machine in - machine.newLoopFromBase3(suspendedBase: continuation) - } - - self.handle(newLoopFromBaseOutput: output) - } - - guard let element3 = try await base3Iterator.next() else { - break - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.base3HasProducedElement(element: element3) - } - - self.handle(baseHasProducedElementOutput: output) - } - } catch { - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseHasProducedFailure(error: error) - } - - self.handle(baseHasProducedFailureOutput: output) - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseIsFinished() - } - - self.handle(baseIsFinishedOutput: output) - } - } - }) - } - } - - private func handle(newLoopFromBaseOutput: ZipStateMachine.NewLoopFromBaseOutput) { - switch newLoopFromBaseOutput { - case .none: - break - - case .resumeBases(let suspendedBases): - suspendedBases.forEach { $0.resume() } - - case .terminate(let task, let suspendedBase, let suspendedDemand): - suspendedBase.resume() - suspendedDemand?.resume(returning: nil) - task?.cancel() - } - } - - private func handle(baseHasProducedElementOutput: ZipStateMachine.BaseHasProducedElementOutput) { - switch baseHasProducedElementOutput { - case .none: - break - - case .resumeDemand(let suspendedDemand, let result1, let result2, let result3): - suspendedDemand?.resume(returning: (result1, result2, result3)) - - case .terminate(let task, let suspendedBases): - suspendedBases?.forEach { $0.resume() } - task?.cancel() - } - } - - private func handle(baseHasProducedFailureOutput: ZipStateMachine.BaseHasProducedFailureOutput) { - switch baseHasProducedFailureOutput { - case .resumeDemandAndTerminate(let task, let suspendedDemand, let suspendedBases, let result1, let result2, let result3): - suspendedDemand?.resume(returning: (result1, result2, result3)) - suspendedBases.forEach { $0.resume() } - task?.cancel() - - case .terminate(let task, let suspendedBases): - suspendedBases?.forEach { $0.resume() } - task?.cancel() - } - } - - private func handle(baseIsFinishedOutput: ZipStateMachine.BaseIsFinishedOutput) { - switch baseIsFinishedOutput { - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0?.resume(returning: nil) } - task?.cancel() - } - } - - func next() async rethrows -> (Base1.Element, Base2.Element, Base3.Element)? { - try await withTaskCancellationHandler { - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.rootTaskIsCancelled() - } - - self.handle(rootTaskIsCancelledOutput: output) - } operation: { - let results = await withUnsafeContinuation { (continuation: UnsafeContinuation<(Result, Result, Result)?, Never>) in - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.newDemandFromConsumer(suspendedDemand: continuation) - } - - self.handle(newDemandFromConsumerOutput: output) - } - - guard let results = results else { - return nil - } - - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.demandIsFulfilled() - } - - self.handle(demandIsFulfilledOutput: output) - - return try (results.0._rethrowGet(), results.1._rethrowGet(), results.2._rethrowGet()) - } - } - - private func handle(rootTaskIsCancelledOutput: ZipStateMachine.RootTaskIsCancelledOutput) { - switch rootTaskIsCancelledOutput { - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0?.resume(returning: nil) } - task?.cancel() - } - } - - private func handle(newDemandFromConsumerOutput: ZipStateMachine.NewDemandFromConsumerOutput) { - switch newDemandFromConsumerOutput { - case .none: - break - - case .resumeBases(let suspendedBases): - suspendedBases.forEach { $0.resume() } - - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0?.resume(returning: nil) } - task?.cancel() - } - } - - private func handle(demandIsFulfilledOutput: ZipStateMachine.DemandIsFulfilledOutput) { - switch demandIsFulfilledOutput { - case .none: - break - - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0.resume(returning: nil) } - task?.cancel() - } - } -} diff --git a/Sources/Combiners/Zip/Zip3StateMachine.swift b/Sources/Combiners/Zip/Zip3StateMachine.swift deleted file mode 100644 index 3d3ed82..0000000 --- a/Sources/Combiners/Zip/Zip3StateMachine.swift +++ /dev/null @@ -1,542 +0,0 @@ -// -// Zip3StateMachine.swift -// -// -// Created by Thibault Wittemberg on 24/09/2022. -// - -struct Zip3StateMachine: Sendable -where Element1: Sendable, Element2: Sendable, Element3: Sendable { - private enum State { - case initial - case started(task: Task) - case awaitingDemandFromConsumer( - task: Task?, - suspendedBases: [UnsafeContinuation] - ) - case awaitingBaseResults( - task: Task?, - result1: Result?, - result2: Result?, - result3: Result?, - suspendedBases: [UnsafeContinuation], - suspendedDemand: UnsafeContinuation<(Result, Result, Result)?, Never>? - ) - case finished - } - - private var state: State = .initial - - mutating func taskIsStarted(task: Task) { - switch self.state { - case .initial: - self.state = .started(task: task) - - default: - assertionFailure("Inconsistent state, the task cannot start while the state is other than initial") - } - } - - enum NewDemandFromConsumerOutput { - case none - case resumeBases(suspendedBases: [UnsafeContinuation]) - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [UnsafeContinuation<(Result, Result, Result)?, Never>?]? - ) - } - - mutating func newDemandFromConsumer( - suspendedDemand: UnsafeContinuation<(Result, Result, Result)?, Never> - ) -> NewDemandFromConsumerOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: [suspendedDemand]) - - case .started(let task): - self.state = .awaitingBaseResults( - task: task, - result1: nil, - result2: nil, - result3: nil, - suspendedBases: [], - suspendedDemand: suspendedDemand - ) - return .none - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .awaitingBaseResults( - task: task, - result1: nil, - result2: nil, - result3: nil, - suspendedBases: [], - suspendedDemand: suspendedDemand - ) - return .resumeBases(suspendedBases: suspendedBases) - - case .awaitingBaseResults(let task, _, _, _, let suspendedBases, let oldSuspendedDemand): - assertionFailure("Inconsistent state, a demand is already suspended") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [oldSuspendedDemand, suspendedDemand]) - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: [suspendedDemand]) - } - } - - enum NewLoopFromBaseOutput { - case none - case resumeBases(suspendedBases: [UnsafeContinuation]) - case terminate( - task: Task?, - suspendedBase: UnsafeContinuation, - suspendedDemand: UnsafeContinuation<(Result, Result, Result)?, Never>? - ) - } - - mutating func newLoopFromBase1(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBase: suspendedBase, suspendedDemand: nil) - - case .started(let task): - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: [suspendedBase]) - return .none - - case .awaitingDemandFromConsumer(let task, var suspendedBases): - assert(suspendedBases.count < 3, "There cannot be more than 3 suspended base at the same time") - suspendedBases.append(suspendedBase) - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .awaitingBaseResults(let task, let result1, let result2, let result3, var suspendedBases, let suspendedDemand): - assert(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") - if result1 != nil { - suspendedBases.append(suspendedBase) - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } else { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .resumeBases(suspendedBases: [suspendedBase]) - } - - case .finished: - return .terminate(task: nil, suspendedBase: suspendedBase, suspendedDemand: nil) - } - } - - mutating func newLoopFromBase2(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { - switch state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBase: suspendedBase, suspendedDemand: nil) - - case .started(let task): - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: [suspendedBase]) - return .none - - case .awaitingDemandFromConsumer(let task, var suspendedBases): - assert(suspendedBases.count < 3, "There cannot be more than 3 suspended base at the same time") - suspendedBases.append(suspendedBase) - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .awaitingBaseResults(let task, let result1, let result2, let result3, var suspendedBases, let suspendedDemand): - assert(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") - if result2 != nil { - suspendedBases.append(suspendedBase) - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } else { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .resumeBases(suspendedBases: [suspendedBase]) - } - - case .finished: - return .terminate(task: nil, suspendedBase: suspendedBase, suspendedDemand: nil) - } - } - - mutating func newLoopFromBase3(suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { - switch state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBase: suspendedBase, suspendedDemand: nil) - - case .started(let task): - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: [suspendedBase]) - return .none - - case .awaitingDemandFromConsumer(let task, var suspendedBases): - assert(suspendedBases.count < 3, "There cannot be more than 3 suspended base at the same time") - suspendedBases.append(suspendedBase) - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .awaitingBaseResults(let task, let result1, let result2, let result3, var suspendedBases, let suspendedDemand): - assert(suspendedBases.count < 3, "There cannot be more than 3 suspended bases at the same time") - if result3 != nil { - suspendedBases.append(suspendedBase) - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } else { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .resumeBases(suspendedBases: [suspendedBase]) - } - - case .finished: - return .terminate(task: nil, suspendedBase: suspendedBase, suspendedDemand: nil) - } - } - - enum BaseHasProducedElementOutput { - case none - case resumeDemand( - suspendedDemand: UnsafeContinuation<(Result, Result, Result)?, Never>?, - result1: Result, - result2: Result, - result3: Result - ) - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]? - ) - } - - mutating func base1HasProducedElement(element: Element1) -> BaseHasProducedElementOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil) - - case .started(let task): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases) - - case .awaitingBaseResults(let task, _, let result2, let result3, let suspendedBases, let suspendedDemand): - if let result2 = result2, let result3 = result3 { - self.state = .awaitingBaseResults( - task: task, - result1: .success(element), - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: nil - ) - return .resumeDemand(suspendedDemand: suspendedDemand, result1: .success(element), result2: result2, result3: result3) - } else { - self.state = .awaitingBaseResults( - task: task, - result1: .success(element), - result2: result2, - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } - - case .finished: - return .terminate(task: nil, suspendedBases: nil) - } - } - - enum BaseHasProducedFailureOutput { - case resumeDemandAndTerminate( - task: Task?, - suspendedDemand: UnsafeContinuation<(Result, Result, Result)?, Never>?, - suspendedBases: [UnsafeContinuation], - result1: Result, - result2: Result, - result3: Result - ) - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]? - ) - } - - mutating func base2HasProducedElement(element: Element2) -> BaseHasProducedElementOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil) - - case .started(let task): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases) - - case .awaitingBaseResults(let task, let result1, _, let result3, let suspendedBases, let suspendedDemand): - if let result1 = result1, let result3 = result3 { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: .success(element), - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: nil - ) - return .resumeDemand(suspendedDemand: suspendedDemand, result1: result1, result2: .success(element), result3: result3) - } else { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: .success(element), - result3: result3, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } - - case .finished: - return .terminate(task: nil, suspendedBases: nil) - } - } - - mutating func base3HasProducedElement(element: Element3) -> BaseHasProducedElementOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil) - - case .started(let task): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases) - - case .awaitingBaseResults(let task, let result1, let result2, _, let suspendedBases, let suspendedDemand): - if let result1 = result1, let result2 = result2 { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: .success(element), - suspendedBases: suspendedBases, - suspendedDemand: nil - ) - return .resumeDemand(suspendedDemand: suspendedDemand, result1: result1, result2: result2, result3: .success(element)) - } else { - self.state = .awaitingBaseResults( - task: task, - result1: result1, - result2: result2, - result3: .success(element), - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } - - case .finished: - return .terminate(task: nil, suspendedBases: nil) - } - } - - mutating func baseHasProducedFailure(error: Error) -> BaseHasProducedFailureOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil) - - case .started(let task): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases) - - case .awaitingBaseResults(let task, _, _, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .resumeDemandAndTerminate( - task: task, - suspendedDemand: suspendedDemand, - suspendedBases: suspendedBases, - result1: .failure(error), - result2: .failure(error), - result3: .failure(error) - ) - - case .finished: - return .terminate(task: nil, suspendedBases: nil) - } - } - - enum DemandIsFulfilledOutput { - case none - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [UnsafeContinuation<(Result, Result)?, Never>]? - ) - } - - mutating func demandIsFulfilled() -> DemandIsFulfilledOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - - case .started(let task): - assertionFailure("Inconsistent state, results are not yet available to be acknowledged") - self.state = .finished - return .terminate(task: task, suspendedBases: nil, suspendedDemands: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - assertionFailure("Inconsistent state, results are not yet available to be acknowledged") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) - - case .awaitingBaseResults(let task, let result1, let result2, let result3, let suspendedBases, let suspendedDemand): - assert(suspendedDemand == nil, "Inconsistent state, there cannot be a suspended demand when ackowledging the demand") - assert( - result1 != nil && result2 != nil && result3 != nil, - "Inconsistent state, all results are not yet available to be acknowledged" - ) - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - } - } - - enum RootTaskIsCancelledOutput { - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [UnsafeContinuation<(Result, Result, Result)?, Never>?]? - ) - } - - mutating func rootTaskIsCancelled() -> RootTaskIsCancelledOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - - case .started(let task): - self.state = .finished - return .terminate(task: task, suspendedBases: nil, suspendedDemands: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) - - case .awaitingBaseResults(let task, _, _, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - } - } - - enum BaseIsFinishedOutput { - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [UnsafeContinuation<(Result, Result, Result)?, Never>?]? - ) - } - - mutating func baseIsFinished() -> BaseIsFinishedOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - - case .started(let task): - self.state = .finished - return .terminate(task: task, suspendedBases: nil, suspendedDemands: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) - - case .awaitingBaseResults(let task, _, _, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - } - } -} diff --git a/Sources/Combiners/Zip/ZipRuntime.swift b/Sources/Combiners/Zip/ZipRuntime.swift deleted file mode 100644 index be04f9d..0000000 --- a/Sources/Combiners/Zip/ZipRuntime.swift +++ /dev/null @@ -1,186 +0,0 @@ -// -// ZipRuntime.swift -// -// -// Created by Thibault Wittemberg on 24/09/2022. -// - -final class ZipRuntime: Sendable -where Base: Sendable, Base.Element: Sendable { - typealias StateMachine = ZipStateMachine - - private let stateMachine: ManagedCriticalState - private let indexes = ManagedCriticalState(0) - - init(_ bases: [Base]) { - self.stateMachine = ManagedCriticalState(StateMachine(numberOfBases: bases.count)) - - self.stateMachine.withCriticalRegion { machine in - machine.taskIsStarted(task: Task { - await withTaskGroup(of: Void.self) { group in - for base in bases { - let index = self.indexes.withCriticalRegion { indexes -> Int in - defer { indexes += 1 } - return indexes - } - - group.addTask { - var baseIterator = base.makeAsyncIterator() - - do { - while true { - await withUnsafeContinuation { (continuation: UnsafeContinuation) in - let output = self.stateMachine.withCriticalRegion { machine in - machine.newLoopFromBase(index: index, suspendedBase: continuation) - } - - self.handle(newLoopFromBaseOutput: output) - } - - guard let element = try await baseIterator.next() else { - break - } - - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseHasProducedElement(index: index, element: element) - } - - self.handle(baseHasProducedElementOutput: output) - } - } catch { - let output = self.stateMachine.withCriticalRegion { machine in - machine.baseHasProducedFailure(error: error) - } - - self.handle(baseHasProducedFailureOutput: output) - } - - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.baseIsFinished() - } - - self.handle(baseIsFinishedOutput: output) - } - } - } - }) - } - } - - private func handle(newLoopFromBaseOutput: StateMachine.NewLoopFromBaseOutput) { - switch newLoopFromBaseOutput { - case .none: - break - - case .resumeBases(let suspendedBases): - suspendedBases.forEach { $0.resume() } - - case .terminate(let task, let suspendedBase, let suspendedDemand): - suspendedBase.resume() - suspendedDemand?.resume(returning: nil) - task?.cancel() - } - } - - private func handle(baseHasProducedElementOutput: StateMachine.BaseHasProducedElementOutput) { - switch baseHasProducedElementOutput { - case .none: - break - - case .resumeDemand(let suspendedDemand, let results): - suspendedDemand?.resume(returning: results) - - case .terminate(let task, let suspendedBases): - suspendedBases?.forEach { $0.resume() } - task?.cancel() - } - } - - private func handle(baseHasProducedFailureOutput: StateMachine.BaseHasProducedFailureOutput) { - switch baseHasProducedFailureOutput { - case .resumeDemandAndTerminate(let task, let suspendedDemand, let suspendedBases, let results): - suspendedDemand?.resume(returning: results) - suspendedBases.forEach { $0.resume() } - task?.cancel() - - case .terminate(let task, let suspendedBases): - suspendedBases?.forEach { $0.resume() } - task?.cancel() - } - } - - private func handle(baseIsFinishedOutput: StateMachine.BaseIsFinishedOutput) { - switch baseIsFinishedOutput { - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0?.resume(returning: nil) } - task?.cancel() - } - } - - func next() async rethrows -> [Base.Element]? { - try await withTaskCancellationHandler { - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.rootTaskIsCancelled() - } - - self.handle(rootTaskIsCancelledOutput: output) - } operation: { - let results = await withUnsafeContinuation { (continuation: UnsafeContinuation<[Int: Result]?, Never>) in - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.newDemandFromConsumer(suspendedDemand: continuation) - } - - self.handle(newDemandFromConsumerOutput: output) - } - - guard let results = results else { - return nil - } - - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.demandIsFulfilled() - } - - self.handle(demandIsFulfilledOutput: output) - - return try results.sorted { $0.key < $1.key }.map { try $0.value._rethrowGet() } - } - } - - private func handle(rootTaskIsCancelledOutput: StateMachine.RootTaskIsCancelledOutput) { - switch rootTaskIsCancelledOutput { - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0?.resume(returning: nil) } - task?.cancel() - } - } - - private func handle(newDemandFromConsumerOutput: StateMachine.NewDemandFromConsumerOutput) { - switch newDemandFromConsumerOutput { - case .none: - break - - case .resumeBases(let suspendedBases): - suspendedBases.forEach { $0.resume() } - - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0?.resume(returning: nil) } - task?.cancel() - } - } - - private func handle(demandIsFulfilledOutput: StateMachine.DemandIsFulfilledOutput) { - switch demandIsFulfilledOutput { - case .none: - break - - case .terminate(let task, let suspendedBases, let suspendedDemands): - suspendedBases?.forEach { $0.resume() } - suspendedDemands?.forEach { $0.resume(returning: nil) } - task?.cancel() - } - } -} diff --git a/Sources/Combiners/Zip/ZipStateMachine.swift b/Sources/Combiners/Zip/ZipStateMachine.swift deleted file mode 100644 index 41e4461..0000000 --- a/Sources/Combiners/Zip/ZipStateMachine.swift +++ /dev/null @@ -1,335 +0,0 @@ -// -// ZipStateMachine.swift -// -// -// Created by Thibault Wittemberg on 24/09/2022. -// - -struct ZipStateMachine: Sendable -where Element: Sendable { - private enum State { - case initial - case started(task: Task) - case awaitingDemandFromConsumer( - task: Task?, - suspendedBases: [UnsafeContinuation] - ) - case awaitingBaseResults( - task: Task?, - results: [Int: Result]?, - suspendedBases: [UnsafeContinuation], - suspendedDemand: UnsafeContinuation<[Int: Result]?, Never>? - ) - case finished - } - - private var state: State = .initial - - let numberOfBases: Int - - init(numberOfBases: Int) { - self.numberOfBases = numberOfBases - } - - mutating func taskIsStarted(task: Task) { - switch self.state { - case .initial: - self.state = .started(task: task) - - default: - assertionFailure("Inconsistent state, the task cannot start while the state is other than initial") - } - } - - enum NewDemandFromConsumerOutput { - case none - case resumeBases(suspendedBases: [UnsafeContinuation]) - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [UnsafeContinuation<[Int: Result]?, Never>?]? - ) - } - - mutating func newDemandFromConsumer( - suspendedDemand: UnsafeContinuation<[Int: Result]?, Never> - ) -> NewDemandFromConsumerOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: [suspendedDemand]) - - case .started(let task): - self.state = .awaitingBaseResults(task: task, results: nil, suspendedBases: [], suspendedDemand: suspendedDemand) - return .none - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .awaitingBaseResults(task: task, results: nil, suspendedBases: [], suspendedDemand: suspendedDemand) - return .resumeBases(suspendedBases: suspendedBases) - - case .awaitingBaseResults(let task, _, let suspendedBases, let oldSuspendedDemand): - assertionFailure("Inconsistent state, a demand is already suspended") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [oldSuspendedDemand, suspendedDemand]) - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: [suspendedDemand]) - } - } - - enum NewLoopFromBaseOutput { - case none - case resumeBases(suspendedBases: [UnsafeContinuation]) - case terminate( - task: Task?, - suspendedBase: UnsafeContinuation, - suspendedDemand: UnsafeContinuation<[Int: Result]?, Never>? - ) - } - - mutating func newLoopFromBase(index: Int, suspendedBase: UnsafeContinuation) -> NewLoopFromBaseOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBase: suspendedBase, suspendedDemand: nil) - - case .started(let task): - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: [suspendedBase]) - return .none - - case .awaitingDemandFromConsumer(let task, var suspendedBases): - assert( - suspendedBases.count < self.numberOfBases, - "There cannot be more than \(self.numberOfBases) suspended base at the same time" - ) - suspendedBases.append(suspendedBase) - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .awaitingBaseResults(let task, let results, var suspendedBases, let suspendedDemand): - assert( - suspendedBases.count < self.numberOfBases, - "There cannot be more than \(self.numberOfBases) suspended base at the same time" - ) - if results?[index] != nil { - suspendedBases.append(suspendedBase) - self.state = .awaitingBaseResults( - task: task, - results: results, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .none - } else { - self.state = .awaitingBaseResults( - task: task, - results: results, - suspendedBases: suspendedBases, - suspendedDemand: suspendedDemand - ) - return .resumeBases(suspendedBases: [suspendedBase]) - } - - case .finished: - return .terminate(task: nil, suspendedBase: suspendedBase, suspendedDemand: nil) - } - } - - enum BaseHasProducedElementOutput { - case none - case resumeDemand( - suspendedDemand: UnsafeContinuation<[Int: Result]?, Never>?, - results: [Int: Result] - ) - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]? - ) - } - - mutating func baseHasProducedElement(index: Int, element: Element) -> BaseHasProducedElementOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil) - - case .started(let task): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases) - - case .awaitingBaseResults(let task, let results, let suspendedBases, let suspendedDemand): - assert(results?[index] == nil, "Inconsistent state, a base can only produce an element when the previous one has been consumed") - var mutableResults: [Int: Result] - if let results = results { - mutableResults = results - } else { - mutableResults = [:] - } - mutableResults[index] = .success(element) - if mutableResults.count == self.numberOfBases { - self.state = .awaitingBaseResults(task: task, results: mutableResults, suspendedBases: suspendedBases, suspendedDemand: nil) - return .resumeDemand(suspendedDemand: suspendedDemand, results: mutableResults) - } else { - self.state = .awaitingBaseResults(task: task, results: mutableResults, suspendedBases: suspendedBases, suspendedDemand: suspendedDemand) - return .none - } - - case .finished: - return .terminate(task: nil, suspendedBases: nil) - } - } - - enum BaseHasProducedFailureOutput { - case resumeDemandAndTerminate( - task: Task?, - suspendedDemand: UnsafeContinuation<[Int: Result]?, Never>?, - suspendedBases: [UnsafeContinuation], - results: [Int: Result] - ) - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]? - ) - } - - mutating func baseHasProducedFailure(error: any Error) -> BaseHasProducedFailureOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil) - - case .started(let task): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - assertionFailure("Inconsistent state, a base can only produce an element when the consumer is awaiting for it") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases) - - case .awaitingBaseResults(let task, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .resumeDemandAndTerminate( - task: task, - suspendedDemand: suspendedDemand, - suspendedBases: suspendedBases, - results: [0: .failure(error)] - ) - - case .finished: - return .terminate(task: nil, suspendedBases: nil) - } - } - - enum DemandIsFulfilledOutput { - case none - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [UnsafeContinuation<[Int: Result]?, Never>]? - ) - } - - mutating func demandIsFulfilled() -> DemandIsFulfilledOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - - case .started(let task): - assertionFailure("Inconsistent state, results are not yet available to be acknowledged") - self.state = .finished - return .terminate(task: task, suspendedBases: nil, suspendedDemands: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - assertionFailure("Inconsistent state, results are not yet available to be acknowledged") - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) - - case .awaitingBaseResults(let task, let results, let suspendedBases, let suspendedDemand): - assert(suspendedDemand == nil, "Inconsistent state, there cannot be a suspended demand when ackowledging the demand") - assert(results?.count == self.numberOfBases, "Inconsistent state, all results are not yet available to be acknowledged") - self.state = .awaitingDemandFromConsumer(task: task, suspendedBases: suspendedBases) - return .none - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - } - } - - enum RootTaskIsCancelledOutput { - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [UnsafeContinuation<[Int: Result]?, Never>?]? - ) - } - - mutating func rootTaskIsCancelled() -> RootTaskIsCancelledOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - - case .started(let task): - self.state = .finished - return .terminate(task: task, suspendedBases: nil, suspendedDemands: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) - - case .awaitingBaseResults(let task, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - } - } - - enum BaseIsFinishedOutput { - case terminate( - task: Task?, - suspendedBases: [UnsafeContinuation]?, - suspendedDemands: [UnsafeContinuation<[Int: Result]?, Never>?]? - ) - } - - mutating func baseIsFinished() -> BaseIsFinishedOutput { - switch self.state { - case .initial: - assertionFailure("Inconsistent state, the task is not started") - self.state = .finished - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - - case .started(let task): - self.state = .finished - return .terminate(task: task, suspendedBases: nil, suspendedDemands: nil) - - case .awaitingDemandFromConsumer(let task, let suspendedBases): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: nil) - - case .awaitingBaseResults(let task, _, let suspendedBases, let suspendedDemand): - self.state = .finished - return .terminate(task: task, suspendedBases: suspendedBases, suspendedDemands: [suspendedDemand]) - - case .finished: - return .terminate(task: nil, suspendedBases: nil, suspendedDemands: nil) - } - } -} diff --git a/Sources/Creators/AsyncLazySequence.swift b/Sources/Creators/AsyncLazySequence.swift deleted file mode 100644 index b68d998..0000000 --- a/Sources/Creators/AsyncLazySequence.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// AsyncLazySequence.swift -// -// -// Created by Thibault Wittemberg on 01/01/2022. -// - -public extension Sequence { - /// Creates an AsyncSequence of the sequence elements. - /// - Returns: The AsyncSequence that outputs the elements from the sequence. - var async: AsyncLazySequence { - AsyncLazySequence(self) - } -} - -/// `AsyncLazySequence` is an AsyncSequence that outputs elements from a traditional Sequence. -/// If the parent task is cancelled while iterating then the iteration finishes. -/// -/// ``` -/// let fromSequence = AsyncLazySequence([1, 2, 3, 4, 5]) -/// -/// for await element in fromSequence { -/// print(element) // will print 1 2 3 4 5 -/// } -/// ``` -public struct AsyncLazySequence: AsyncSequence { - public typealias Element = Base.Element - public typealias AsyncIterator = Iterator - - private var base: Base - - public init(_ base: Base) { - self.base = base - } - - public func makeAsyncIterator() -> AsyncIterator { - Iterator(base: self.base.makeIterator()) - } - - public struct Iterator: AsyncIteratorProtocol { - var base: Base.Iterator - - public mutating func next() async -> Base.Element? { - guard !Task.isCancelled else { return nil } - return self.base.next() - } - } -} - -extension AsyncLazySequence: Sendable where Base: Sendable {} -extension AsyncLazySequence.Iterator: Sendable where Base.Iterator: Sendable {} diff --git a/Sources/Creators/AsyncTimerSequence.swift b/Sources/Creators/AsyncTimerSequence.swift index 5633d82..c45c5f4 100644 --- a/Sources/Creators/AsyncTimerSequence.swift +++ b/Sources/Creators/AsyncTimerSequence.swift @@ -5,7 +5,12 @@ // Created by Thibault Wittemberg on 04/03/2022. // +#if canImport(FoundationEssentials) +import FoundationEssentials +import Dispatch +#else @preconcurrency import Foundation +#endif private extension DispatchTimeInterval { var nanoseconds: UInt64 { @@ -78,11 +83,11 @@ public struct AsyncTimerSequence: AsyncSequence { } public mutating func next() async -> Element? { - await withTaskCancellationHandler { [task] in - task.cancel() - } operation: { + await withTaskCancellationHandler { guard !Task.isCancelled else { return nil } return await self.iterator.next() + } onCancel: { [task] in + task.cancel() } } } diff --git a/Sources/Operators/AsyncMulticastSequence.swift b/Sources/Operators/AsyncMulticastSequence.swift index 9fd6a32..16a5db9 100644 --- a/Sources/Operators/AsyncMulticastSequence.swift +++ b/Sources/Operators/AsyncMulticastSequence.swift @@ -105,37 +105,31 @@ where Base.Element == Subject.Element, Subject.Failure == Error, Base.AsyncItera } func next() async { - await Task { - let (canAccessBase, iterator) = self.state.withCriticalRegion { state -> (Bool, Base.AsyncIterator?) in - switch state { - case .available(let iterator): - state = .busy - return (true, iterator) - case .busy: - return (false, nil) - } + let (canAccessBase, iterator) = self.state.withCriticalRegion { state -> (Bool, Base.AsyncIterator?) in + switch state { + case .available(let iterator): + state = .busy + return (true, iterator) + case .busy: + return (false, nil) } + } - guard canAccessBase, var iterator = iterator else { return } - - let toSend: Result - do { - let element = try await iterator.next() - toSend = .success(element) - } catch { - toSend = .failure(error) - } + guard canAccessBase, var iterator = iterator else { return } - self.state.withCriticalRegion { state in - state = .available(iterator) + do { + if let element = try await iterator.next() { + self.subject.send(element) + } else { + self.subject.send(.finished) } + } catch { + self.subject.send(.failure(error)) + } - switch toSend { - case .success(.some(let element)): self.subject.send(element) - case .success(.none): self.subject.send(.finished) - case .failure(let error): self.subject.send(.failure(error)) - } - }.value + self.state.withCriticalRegion { state in + state = .available(iterator) + } } public func makeAsyncIterator() -> AsyncIterator { @@ -155,8 +149,6 @@ where Base.Element == Subject.Element, Subject.Failure == Error, Base.AsyncItera let isConnected: ManagedCriticalState public mutating func next() async rethrows -> Element? { - guard !Task.isCancelled else { return nil } - let shouldWaitForGate = self.isConnected.withCriticalRegion { isConnected -> Bool in if !isConnected { isConnected = true @@ -169,11 +161,10 @@ where Base.Element == Subject.Element, Subject.Failure == Error, Base.AsyncItera } if !self.subjectIterator.hasBufferedElements { - await self.asyncMulticastSequence.next() + await self.asyncMulticastSequence.next() } - let element = try await self.subjectIterator.next() - return element + return try await self.subjectIterator.next() } } } diff --git a/Sources/Operators/AsyncSwitchToLatestSequence.swift b/Sources/Operators/AsyncSwitchToLatestSequence.swift index 736df25..30147b8 100644 --- a/Sources/Operators/AsyncSwitchToLatestSequence.swift +++ b/Sources/Operators/AsyncSwitchToLatestSequence.swift @@ -221,12 +221,7 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl guard !Task.isCancelled else { return nil } self.startBase() - return try await withTaskCancellationHandler { [baseTask, state] in - baseTask?.cancel() - state.withCriticalRegion { - $0.childTask?.cancel() - } - } operation: { + return try await withTaskCancellationHandler { while true { let childTask = await withUnsafeContinuation { [state] (continuation: UnsafeContinuation?, Never>) in let decision = state.withCriticalRegion { state -> NextDecision in @@ -303,6 +298,11 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl return try element._rethrowGet() } } + } onCancel: { [baseTask, state] in + baseTask?.cancel() + state.withCriticalRegion { + $0.childTask?.cancel() + } } } } diff --git a/Sources/Supporting/Locking.swift b/Sources/Supporting/Locking.swift new file mode 100644 index 0000000..d70a71b --- /dev/null +++ b/Sources/Supporting/Locking.swift @@ -0,0 +1,154 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(WinSDK) +import WinSDK +#elseif canImport(Android) +import Android +#endif + +internal struct Lock { +#if canImport(Darwin) + typealias Primitive = os_unfair_lock +#elseif canImport(Glibc) || canImport(Android) + typealias Primitive = pthread_mutex_t +#elseif canImport(WinSDK) + typealias Primitive = SRWLOCK +#endif + + typealias PlatformLock = UnsafeMutablePointer + let platformLock: PlatformLock + + private init(_ platformLock: PlatformLock) { + self.platformLock = platformLock + } + + fileprivate static func initialize(_ platformLock: PlatformLock) { +#if canImport(Darwin) + platformLock.initialize(to: os_unfair_lock()) +#elseif canImport(Glibc) || canImport(Android) + let result = pthread_mutex_init(platformLock, nil) + precondition(result == 0, "pthread_mutex_init failed") +#elseif canImport(WinSDK) + InitializeSRWLock(platformLock) +#endif + } + + fileprivate static func deinitialize(_ platformLock: PlatformLock) { +#if canImport(Glibc) || canImport(Android) + let result = pthread_mutex_destroy(platformLock) + precondition(result == 0, "pthread_mutex_destroy failed") +#endif + platformLock.deinitialize(count: 1) + } + + fileprivate static func lock(_ platformLock: PlatformLock) { +#if canImport(Darwin) + os_unfair_lock_lock(platformLock) +#elseif canImport(Glibc) || canImport(Android) + pthread_mutex_lock(platformLock) +#elseif canImport(WinSDK) + AcquireSRWLockExclusive(platformLock) +#endif + } + + fileprivate static func unlock(_ platformLock: PlatformLock) { +#if canImport(Darwin) + os_unfair_lock_unlock(platformLock) +#elseif canImport(Glibc) || canImport(Android) + let result = pthread_mutex_unlock(platformLock) + precondition(result == 0, "pthread_mutex_unlock failed") +#elseif canImport(WinSDK) + ReleaseSRWLockExclusive(platformLock) +#endif + } + + static func allocate() -> Lock { + let platformLock = PlatformLock.allocate(capacity: 1) + initialize(platformLock) + return Lock(platformLock) + } + + func deinitialize() { + Lock.deinitialize(platformLock) + } + + func lock() { + Lock.lock(platformLock) + } + + func unlock() { + Lock.unlock(platformLock) + } + + /// Acquire the lock for the duration of the given block. + /// + /// This convenience method should be preferred to `lock` and `unlock` in + /// most situations, as it ensures that the lock will be released regardless + /// of how `body` exits. + /// + /// - Parameter body: The block to execute while holding the lock. + /// - Returns: The value returned by the block. + func withLock(_ body: () throws -> T) rethrows -> T { + self.lock() + defer { + self.unlock() + } + return try body() + } + + // specialise Void return (for performance) + func withLockVoid(_ body: () throws -> Void) rethrows -> Void { + try self.withLock(body) + } +} + +struct ManagedCriticalState { + private final class LockedBuffer: ManagedBuffer { + deinit { + withUnsafeMutablePointerToElements { Lock.deinitialize($0) } + } + } + + private let buffer: ManagedBuffer + + init(_ initial: State) { + buffer = LockedBuffer.create(minimumCapacity: 1) { buffer in + buffer.withUnsafeMutablePointerToElements { Lock.initialize($0) } + return initial + } + } + + @discardableResult + func withCriticalRegion(_ critical: (inout State) throws -> R) rethrows -> R { + try buffer.withUnsafeMutablePointers { header, lock in + Lock.lock(lock) + defer { Lock.unlock(lock) } + return try critical(&header.pointee) + } + } + + func apply(criticalState newState: State) { + self.withCriticalRegion { actual in + actual = newState + } + } + + var criticalState: State { + self.withCriticalRegion { $0 } + } +} + +extension ManagedCriticalState: @unchecked Sendable where State: Sendable { } diff --git a/Sources/Supporting/ManagedCriticalState.swift b/Sources/Supporting/ManagedCriticalState.swift deleted file mode 100644 index 102b7d0..0000000 --- a/Sources/Supporting/ManagedCriticalState.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Darwin - -final class LockedBuffer: ManagedBuffer { - deinit { - _ = self.withUnsafeMutablePointerToElements { lock in - lock.deinitialize(count: 1) - } - } -} - -struct ManagedCriticalState { - let buffer: ManagedBuffer - - init(_ initial: State) { - buffer = LockedBuffer.create(minimumCapacity: 1) { buffer in - buffer.withUnsafeMutablePointerToElements { lock in - lock.initialize(to: os_unfair_lock()) - } - return initial - } - } - - @discardableResult - func withCriticalRegion( - _ critical: (inout State) throws -> R - ) rethrows -> R { - try buffer.withUnsafeMutablePointers { header, lock in - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - return try critical(&header.pointee) - } - } - - func apply(criticalState newState: State) { - self.withCriticalRegion { actual in - actual = newState - } - } - - var criticalState: State { - self.withCriticalRegion { $0 } - } -} - -extension ManagedCriticalState: @unchecked Sendable where State: Sendable { } diff --git a/Sources/Supporting/Regulator.swift b/Sources/Supporting/Regulator.swift index 1e11f2b..456469f 100644 --- a/Sources/Supporting/Regulator.swift +++ b/Sources/Supporting/Regulator.swift @@ -48,8 +48,6 @@ final class Regulator: @unchecked Sendable { func iterate() async { await withTaskCancellationHandler { - self.unsuspendAndExitOnCancel() - } operation: { var mutableBase = base.makeAsyncIterator() do { @@ -99,6 +97,8 @@ final class Regulator: @unchecked Sendable { } self.onNextRegulatedElement(.element(result: .failure(error))) } + } onCancel: { + self.unsuspendAndExitOnCancel() } } diff --git a/Tests/AsyncChannels/AsyncBufferedChannelTests.swift b/Tests/AsyncChannels/AsyncBufferedChannelTests.swift index a032c89..1476e76 100644 --- a/Tests/AsyncChannels/AsyncBufferedChannelTests.swift +++ b/Tests/AsyncChannels/AsyncBufferedChannelTests.swift @@ -46,7 +46,7 @@ final class AsyncBufferedChannelTests: XCTestCase { return received } - wait(for: [iterationIsAwaiting], timeout: 1.0) + await fulfillment(of: [iterationIsAwaiting], timeout: 1.0) // When sut.send(1) @@ -125,19 +125,19 @@ final class AsyncBufferedChannelTests: XCTestCase { for await element in sut { received = element taskCanBeCancelled.fulfill() - wait(for: [taskWasCancelled], timeout: 1.0) + await fulfillment(of: [taskWasCancelled], timeout: 1.0) } iterationHasFinished.fulfill() return received } - wait(for: [taskCanBeCancelled], timeout: 1.0) + await fulfillment(of: [taskCanBeCancelled], timeout: 1.0) // When task.cancel() taskWasCancelled.fulfill() - wait(for: [iterationHasFinished], timeout: 1.0) + await fulfillment(of: [iterationHasFinished], timeout: 1.0) // Then let received = await task.value @@ -170,12 +170,12 @@ final class AsyncBufferedChannelTests: XCTestCase { return received } - wait(for: [iteration1IsAwaiting, iteration2IsAwaiting], timeout: 1.0) + await fulfillment(of: [iteration1IsAwaiting, iteration2IsAwaiting], timeout: 1.0) // When sut.finish() - wait(for: [iteration1IsFinished, iteration2IsFinished], timeout: 1.0) + await fulfillment(of: [iteration1IsFinished, iteration2IsFinished], timeout: 1.0) let received1 = await task1.value let received2 = await task2.value @@ -217,7 +217,7 @@ final class AsyncBufferedChannelTests: XCTestCase { }.cancel() // Then - wait(for: [iterationIsFinished], timeout: 1.0) + await fulfillment(of: [iterationIsFinished], timeout: 1.0) } func test_awaiting_uses_id_for_equatable() { diff --git a/Tests/AsyncChannels/AsyncBufferedThrowingChannelTests.swift b/Tests/AsyncChannels/AsyncBufferedThrowingChannelTests.swift index 7bef3ec..1b01c9d 100644 --- a/Tests/AsyncChannels/AsyncBufferedThrowingChannelTests.swift +++ b/Tests/AsyncChannels/AsyncBufferedThrowingChannelTests.swift @@ -46,7 +46,7 @@ final class AsyncBufferedThrowingChannelTests: XCTestCase { return received } - wait(for: [iterationIsAwaiting], timeout: 1.0) + await fulfillment(of: [iterationIsAwaiting], timeout: 1.0) // When sut.send(1) @@ -125,19 +125,19 @@ final class AsyncBufferedThrowingChannelTests: XCTestCase { for try await element in sut { received = element taskCanBeCancelled.fulfill() - wait(for: [taskWasCancelled], timeout: 1.0) + await fulfillment(of: [taskWasCancelled], timeout: 1.0) } iterationHasFinished.fulfill() return received } - wait(for: [taskCanBeCancelled], timeout: 1.0) + await fulfillment(of: [taskCanBeCancelled], timeout: 1.0) // When task.cancel() taskWasCancelled.fulfill() - wait(for: [iterationHasFinished], timeout: 1.0) + await fulfillment(of: [iterationHasFinished], timeout: 1.0) // Then let received = try await task.value @@ -211,13 +211,13 @@ final class AsyncBufferedThrowingChannelTests: XCTestCase { } } - wait(for: [iteration1IsAwaiting, iteration2IsAwaiting], timeout: 1.0) + await fulfillment(of: [iteration1IsAwaiting, iteration2IsAwaiting], timeout: 1.0) // When sut.fail(MockError(code: 1701)) // Then - wait(for: [iteration1HasThrown, iteration2HasThrown], timeout: 1.0) + await fulfillment(of: [iteration1HasThrown, iteration2HasThrown], timeout: 1.0) let iterator = sut.makeAsyncIterator() do { @@ -254,12 +254,12 @@ final class AsyncBufferedThrowingChannelTests: XCTestCase { return received } - wait(for: [iteration1IsAwaiting, iteration2IsAwaiting], timeout: 1.0) + await fulfillment(of: [iteration1IsAwaiting, iteration2IsAwaiting], timeout: 1.0) // When sut.finish() - wait(for: [iteration1IsFinished, iteration2IsFinished], timeout: 1.0) + await fulfillment(of: [iteration1IsFinished, iteration2IsFinished], timeout: 1.0) let received1 = try await task1.value let received2 = try await task2.value @@ -301,7 +301,7 @@ final class AsyncBufferedThrowingChannelTests: XCTestCase { }.cancel() // Then - wait(for: [iterationIsFinished], timeout: 1.0) + await fulfillment(of: [iterationIsFinished], timeout: 1.0) } func test_awaiting_uses_id_for_equatable() { diff --git a/Tests/AsyncSubjets/AsyncCurrentValueSubjectTests.swift b/Tests/AsyncSubjets/AsyncCurrentValueSubjectTests.swift index cf6a9c8..0ff8abe 100644 --- a/Tests/AsyncSubjets/AsyncCurrentValueSubjectTests.swift +++ b/Tests/AsyncSubjets/AsyncCurrentValueSubjectTests.swift @@ -31,7 +31,7 @@ final class AsyncCurrentValueSubjectTests: XCTestCase { XCTAssertEqual(received2, 1) } - func test_send_pushes_values_in_the_subject() { + func test_send_pushes_values_in_the_subject() async { let hasReceivedOneElementExpectation = expectation(description: "One element has been iterated in the async sequence") hasReceivedOneElementExpectation.expectedFulfillmentCount = 2 @@ -72,12 +72,12 @@ final class AsyncCurrentValueSubjectTests: XCTestCase { } } - wait(for: [hasReceivedOneElementExpectation], timeout: 1) + await fulfillment(of: [hasReceivedOneElementExpectation], timeout: 1) sut.send(2) sut.value = 3 - wait(for: [hasReceivedSentElementsExpectation], timeout: 1) + await fulfillment(of: [hasReceivedSentElementsExpectation], timeout: 1) } func test_sendFinished_ends_the_subject_and_immediately_resumes_futur_consumer() async { @@ -107,18 +107,18 @@ final class AsyncCurrentValueSubjectTests: XCTestCase { hasFinishedExpectation.fulfill() } - wait(for: [hasReceivedOneElementExpectation], timeout: 1) + await fulfillment(of: [hasReceivedOneElementExpectation], timeout: 1) sut.send(.finished) - wait(for: [hasFinishedExpectation], timeout: 1) + await fulfillment(of: [hasFinishedExpectation], timeout: 1) var iterator = sut.makeAsyncIterator() let received = await iterator.next() XCTAssertNil(received) } - func test_subject_finishes_when_task_is_cancelled() { + func test_subject_finishes_when_task_is_cancelled() async { let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExceptation = expectation(description: "The task has been cancelled") let taskHasFinishedExpectation = expectation(description: "The task has finished") @@ -130,19 +130,19 @@ final class AsyncCurrentValueSubjectTests: XCTestCase { for await element in sut { firstElement = element canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) + await fulfillment(of: [hasCancelExceptation], timeout: 5) } XCTAssertEqual(firstElement, 1) taskHasFinishedExpectation.fulfill() } - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task + await fulfillment(of: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task task.cancel() hasCancelExceptation.fulfill() // we can release the lock in the for loop - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished + await fulfillment(of: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished } func test_subject_handles_concurrency() async throws { @@ -175,7 +175,7 @@ final class AsyncCurrentValueSubjectTests: XCTestCase { return received.sorted() } - await waitForExpectations(timeout: 1) + await fulfillment(of: [canSendExpectation], timeout: 1) // concurrently push values in the sut 1 let task1 = Task { diff --git a/Tests/AsyncSubjets/AsyncPassthroughSubjectTests.swift b/Tests/AsyncSubjets/AsyncPassthroughSubjectTests.swift index 2cbac1c..cfb17ac 100644 --- a/Tests/AsyncSubjets/AsyncPassthroughSubjectTests.swift +++ b/Tests/AsyncSubjets/AsyncPassthroughSubjectTests.swift @@ -9,7 +9,7 @@ import XCTest final class AsyncPassthroughSubjectTests: XCTestCase { - func test_send_pushes_elements_in_the_subject() { + func test_send_pushes_elements_in_the_subject() async { let isReadyToBeIteratedExpectation = expectation(description: "Passthrough subject iterators are ready for iteration") isReadyToBeIteratedExpectation.expectedFulfillmentCount = 2 @@ -48,13 +48,13 @@ final class AsyncPassthroughSubjectTests: XCTestCase { } } - wait(for: [isReadyToBeIteratedExpectation], timeout: 1) + await fulfillment(of: [isReadyToBeIteratedExpectation], timeout: 1) sut.send(1) sut.send(2) sut.send(3) - wait(for: [hasReceivedSentElementsExpectation], timeout: 1) + await fulfillment(of: [hasReceivedSentElementsExpectation], timeout: 1) } func test_sendFinished_ends_the_subject_and_immediately_resumes_futur_consumer() async { @@ -91,22 +91,22 @@ final class AsyncPassthroughSubjectTests: XCTestCase { hasFinishedExpectation.fulfill() } - wait(for: [isReadyToBeIteratedExpectation], timeout: 1) + await fulfillment(of: [isReadyToBeIteratedExpectation], timeout: 1) sut.send(1) - wait(for: [hasReceivedOneElementExpectation], timeout: 1) + await fulfillment(of: [hasReceivedOneElementExpectation], timeout: 1) sut.send( .finished) - wait(for: [hasFinishedExpectation], timeout: 1) + await fulfillment(of: [hasFinishedExpectation], timeout: 1) var iterator = sut.makeAsyncIterator() let received = await iterator.next() XCTAssertNil(received) } - func test_subject_finishes_when_task_is_cancelled() { + func test_subject_finishes_when_task_is_cancelled() async { let isReadyToBeIteratedExpectation = expectation(description: "Passthrough subject iterators are ready for iteration") let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExpectation = expectation(description: "The task has been cancelled") @@ -122,23 +122,23 @@ final class AsyncPassthroughSubjectTests: XCTestCase { while let element = await it.next() { receivedElements.append(element) canCancelExpectation.fulfill() - wait(for: [hasCancelExpectation], timeout: 5) + await fulfillment(of: [hasCancelExpectation], timeout: 5) } XCTAssertEqual(receivedElements, [1]) taskHasFinishedExpectation.fulfill() } - wait(for: [isReadyToBeIteratedExpectation], timeout: 1) + await fulfillment(of: [isReadyToBeIteratedExpectation], timeout: 1) sut.send(1) - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task + await fulfillment(of: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task task.cancel() hasCancelExpectation.fulfill() // we can release the lock in the for loop - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished + await fulfillment(of: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished } func test_subject_handles_concurrency() async { @@ -171,7 +171,7 @@ final class AsyncPassthroughSubjectTests: XCTestCase { return received.sorted() } - await waitForExpectations(timeout: 1) + await fulfillment(of: [canSendExpectation], timeout: 1) // concurrently push values in the sut 1 let task1 = Task { diff --git a/Tests/AsyncSubjets/AsyncReplaySubjectTests.swift b/Tests/AsyncSubjets/AsyncReplaySubjectTests.swift index e4a3857..910f7d0 100644 --- a/Tests/AsyncSubjets/AsyncReplaySubjectTests.swift +++ b/Tests/AsyncSubjets/AsyncReplaySubjectTests.swift @@ -9,7 +9,7 @@ import XCTest final class AsyncReplaySubjectTests: XCTestCase { - func test_send_replays_buffered_elements() { + func test_send_replays_buffered_elements() async { let exp = expectation(description: "Send has stacked elements in the replay the buffer") exp.expectedFulfillmentCount = 2 @@ -47,10 +47,10 @@ final class AsyncReplaySubjectTests: XCTestCase { } } - waitForExpectations(timeout: 0.5) + await fulfillment(of: [exp], timeout: 0.5) } - func test_send_pushes_elements_in_the_subject() { + func test_send_pushes_elements_in_the_subject() async { let hasReceivedOneElementExpectation = expectation(description: "One element has been iterated in the async sequence") hasReceivedOneElementExpectation.expectedFulfillmentCount = 2 @@ -93,12 +93,12 @@ final class AsyncReplaySubjectTests: XCTestCase { } } - wait(for: [hasReceivedOneElementExpectation], timeout: 1) + await fulfillment(of: [hasReceivedOneElementExpectation], timeout: 1) sut.send(2) sut.send(3) - wait(for: [hasReceivedSentElementsExpectation], timeout: 1) + await fulfillment(of: [hasReceivedSentElementsExpectation], timeout: 1) } func test_sendFinished_ends_the_subject_and_immediately_resumes_futur_consumer() async { @@ -130,18 +130,18 @@ final class AsyncReplaySubjectTests: XCTestCase { hasFinishedExpectation.fulfill() } - wait(for: [hasReceivedOneElementExpectation], timeout: 1) + await fulfillment(of: [hasReceivedOneElementExpectation], timeout: 1) sut.send(.finished) - wait(for: [hasFinishedExpectation], timeout: 1) + await fulfillment(of: [hasFinishedExpectation], timeout: 1) var iterator = sut.makeAsyncIterator() let received = await iterator.next() XCTAssertNil(received) } - func test_subject_finishes_when_task_is_cancelled() { + func test_subject_finishes_when_task_is_cancelled() async { let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExceptation = expectation(description: "The task has been cancelled") let taskHasFinishedExpectation = expectation(description: "The task has finished") @@ -155,19 +155,19 @@ final class AsyncReplaySubjectTests: XCTestCase { for await element in sut { firstElement = element canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) + await fulfillment(of: [hasCancelExceptation], timeout: 5) } XCTAssertEqual(firstElement, 1) taskHasFinishedExpectation.fulfill() } - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task + await fulfillment(of: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task task.cancel() hasCancelExceptation.fulfill() // we can release the lock in the for loop - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished + await fulfillment(of: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished } func test_subject_handles_concurrency() async { @@ -200,7 +200,7 @@ final class AsyncReplaySubjectTests: XCTestCase { return received.sorted() } - await waitForExpectations(timeout: 1) + await fulfillment(of: [canSendExpectation], timeout: 1) // concurrently push values in the sut 1 let task1 = Task { diff --git a/Tests/AsyncSubjets/AsyncThrowingCurrentValueSubjectTests.swift b/Tests/AsyncSubjets/AsyncThrowingCurrentValueSubjectTests.swift index 2b323d0..7e03bee 100644 --- a/Tests/AsyncSubjets/AsyncThrowingCurrentValueSubjectTests.swift +++ b/Tests/AsyncSubjets/AsyncThrowingCurrentValueSubjectTests.swift @@ -31,7 +31,7 @@ final class AsyncThrowingCurrentValueSubjectTests: XCTestCase { XCTAssertEqual(received2, 1) } - func test_send_pushes_values_in_the_subject() { + func test_send_pushes_values_in_the_subject() async { let hasReceivedOneElementExpectation = expectation(description: "One element has been iterated in the async sequence") hasReceivedOneElementExpectation.expectedFulfillmentCount = 2 @@ -72,12 +72,12 @@ final class AsyncThrowingCurrentValueSubjectTests: XCTestCase { } } - wait(for: [hasReceivedOneElementExpectation], timeout: 1) + await fulfillment(of: [hasReceivedOneElementExpectation], timeout: 1) sut.send(2) sut.value = 3 - wait(for: [hasReceivedSentElementsExpectation], timeout: 1) + await fulfillment(of: [hasReceivedSentElementsExpectation], timeout: 1) } func test_sendFinished_ends_the_subject_and_immediately_resumes_futur_consumer() async throws { @@ -107,11 +107,11 @@ final class AsyncThrowingCurrentValueSubjectTests: XCTestCase { hasFinishedExpectation.fulfill() } - wait(for: [hasReceivedOneElementExpectation], timeout: 1) + await fulfillment(of: [hasReceivedOneElementExpectation], timeout: 1) sut.send(.finished) - wait(for: [hasFinishedExpectation], timeout: 1) + await fulfillment(of: [hasFinishedExpectation], timeout: 1) var iterator = sut.makeAsyncIterator() let received = try await iterator.next() @@ -155,11 +155,11 @@ final class AsyncThrowingCurrentValueSubjectTests: XCTestCase { } } - wait(for: [hasReceivedOneElementExpectation], timeout: 1) + await fulfillment(of: [hasReceivedOneElementExpectation], timeout: 1) sut.send(.failure(expectedError)) - wait(for: [hasFinishedWithFailureExpectation], timeout: 1) + await fulfillment(of: [hasFinishedWithFailureExpectation], timeout: 1) var iterator = sut.makeAsyncIterator() do { @@ -170,7 +170,7 @@ final class AsyncThrowingCurrentValueSubjectTests: XCTestCase { } } - func test_subject_finishes_when_task_is_cancelled() { + func test_subject_finishes_when_task_is_cancelled() async { let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExceptation = expectation(description: "The task has been cancelled") let taskHasFinishedExpectation = expectation(description: "The task has finished") @@ -182,19 +182,19 @@ final class AsyncThrowingCurrentValueSubjectTests: XCTestCase { for try await element in sut { firstElement = element canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) + await fulfillment(of: [hasCancelExceptation], timeout: 5) } XCTAssertEqual(firstElement, 1) taskHasFinishedExpectation.fulfill() } - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task + await fulfillment(of: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task task.cancel() hasCancelExceptation.fulfill() // we can release the lock in the for loop - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished + await fulfillment(of: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished } func test_subject_handles_concurrency() async throws { @@ -227,7 +227,7 @@ final class AsyncThrowingCurrentValueSubjectTests: XCTestCase { return received.sorted() } - await waitForExpectations(timeout: 1) + await fulfillment(of: [canSendExpectation], timeout: 1) // concurrently push values in the sut 1 let task1 = Task { diff --git a/Tests/AsyncSubjets/AsyncThrowingPassthroughSubjectTests.swift b/Tests/AsyncSubjets/AsyncThrowingPassthroughSubjectTests.swift index 8546868..5e336b8 100644 --- a/Tests/AsyncSubjets/AsyncThrowingPassthroughSubjectTests.swift +++ b/Tests/AsyncSubjets/AsyncThrowingPassthroughSubjectTests.swift @@ -9,7 +9,7 @@ import XCTest final class AsyncThrowingPassthroughSubjectTests: XCTestCase { - func test_send_pushes_elements_in_the_subject() { + func test_send_pushes_elements_in_the_subject() async { let isReadyToBeIteratedExpectation = expectation(description: "Passthrough subject iterators are ready for iteration") isReadyToBeIteratedExpectation.expectedFulfillmentCount = 2 @@ -48,13 +48,13 @@ final class AsyncThrowingPassthroughSubjectTests: XCTestCase { } } - wait(for: [isReadyToBeIteratedExpectation], timeout: 1) + await fulfillment(of: [isReadyToBeIteratedExpectation], timeout: 1) sut.send(1) sut.send(2) sut.send(3) - wait(for: [hasReceivedSentElementsExpectation], timeout: 1) + await fulfillment(of: [hasReceivedSentElementsExpectation], timeout: 1) } func test_sendFinished_ends_the_subject_and_immediately_resumes_futur_consumer() async throws { @@ -91,15 +91,15 @@ final class AsyncThrowingPassthroughSubjectTests: XCTestCase { hasFinishedExpectation.fulfill() } - wait(for: [isReadyToBeIteratedExpectation], timeout: 1) + await fulfillment(of: [isReadyToBeIteratedExpectation], timeout: 1) sut.send(1) - wait(for: [hasReceivedOneElementExpectation], timeout: 1) + await fulfillment(of: [hasReceivedOneElementExpectation], timeout: 1) sut.send( .finished) - wait(for: [hasFinishedExpectation], timeout: 1) + await fulfillment(of: [hasFinishedExpectation], timeout: 1) var iterator = sut.makeAsyncIterator() let received = try await iterator.next() @@ -150,15 +150,15 @@ final class AsyncThrowingPassthroughSubjectTests: XCTestCase { } } - wait(for: [isReadyToBeIteratedExpectation], timeout: 1) + await fulfillment(of: [isReadyToBeIteratedExpectation], timeout: 1) sut.send(1) - wait(for: [hasReceivedOneElementExpectation], timeout: 1) + await fulfillment(of: [hasReceivedOneElementExpectation], timeout: 1) sut.send(.failure(expectedError)) - wait(for: [hasFinishedWithFailureExpectation], timeout: 1) + await fulfillment(of: [hasFinishedWithFailureExpectation], timeout: 1) var iterator = sut.makeAsyncIterator() do { @@ -169,7 +169,7 @@ final class AsyncThrowingPassthroughSubjectTests: XCTestCase { } } - func test_subject_finishes_when_task_is_cancelled() { + func test_subject_finishes_when_task_is_cancelled() async { let isReadyToBeIteratedExpectation = expectation(description: "Passthrough subject iterators are ready for iteration") let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExpectation = expectation(description: "The task has been cancelled") @@ -185,23 +185,23 @@ final class AsyncThrowingPassthroughSubjectTests: XCTestCase { while let element = try await it.next() { receivedElements.append(element) canCancelExpectation.fulfill() - wait(for: [hasCancelExpectation], timeout: 5) + await fulfillment(of: [hasCancelExpectation], timeout: 5) } XCTAssertEqual(receivedElements, [1]) taskHasFinishedExpectation.fulfill() } - wait(for: [isReadyToBeIteratedExpectation], timeout: 1) + await fulfillment(of: [isReadyToBeIteratedExpectation], timeout: 1) sut.send(1) - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task + await fulfillment(of: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task task.cancel() hasCancelExpectation.fulfill() // we can release the lock in the for loop - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished + await fulfillment(of: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished } func test_subject_handles_concurrency() async throws { @@ -234,7 +234,7 @@ final class AsyncThrowingPassthroughSubjectTests: XCTestCase { return received.sorted() } - await waitForExpectations(timeout: 1) + await fulfillment(of: [canSendExpectation], timeout: 1) // concurrently push values in the sut 1 let task1 = Task { diff --git a/Tests/AsyncSubjets/AsyncThrowingReplaySubjectTests.swift b/Tests/AsyncSubjets/AsyncThrowingReplaySubjectTests.swift index df5edde..110e7b2 100644 --- a/Tests/AsyncSubjets/AsyncThrowingReplaySubjectTests.swift +++ b/Tests/AsyncSubjets/AsyncThrowingReplaySubjectTests.swift @@ -9,7 +9,7 @@ import XCTest final class AsyncThrowingReplaySubjectTests: XCTestCase { - func test_send_replays_buffered_elements() { + func test_send_replays_buffered_elements() async { let exp = expectation(description: "Send has stacked elements in the replay the buffer") exp.expectedFulfillmentCount = 2 @@ -47,10 +47,10 @@ final class AsyncThrowingReplaySubjectTests: XCTestCase { } } - waitForExpectations(timeout: 0.5) + await fulfillment(of: [exp], timeout: 0.5) } - func test_send_pushes_elements_in_the_subject() { + func test_send_pushes_elements_in_the_subject() async { let hasReceivedOneElementExpectation = expectation(description: "One element has been iterated in the async sequence") hasReceivedOneElementExpectation.expectedFulfillmentCount = 2 @@ -93,12 +93,12 @@ final class AsyncThrowingReplaySubjectTests: XCTestCase { } } - wait(for: [hasReceivedOneElementExpectation], timeout: 1) + await fulfillment(of: [hasReceivedOneElementExpectation], timeout: 1) sut.send(2) sut.send(3) - wait(for: [hasReceivedSentElementsExpectation], timeout: 1) + await fulfillment(of: [hasReceivedSentElementsExpectation], timeout: 1) } func test_sendFinished_ends_the_subject_and_immediately_resumes_futur_consumer() async throws { @@ -130,11 +130,11 @@ final class AsyncThrowingReplaySubjectTests: XCTestCase { hasFinishedExpectation.fulfill() } - wait(for: [hasReceivedOneElementExpectation], timeout: 1) + await fulfillment(of: [hasReceivedOneElementExpectation], timeout: 1) sut.send(.finished) - wait(for: [hasFinishedExpectation], timeout: 1) + await fulfillment(of: [hasFinishedExpectation], timeout: 1) var iterator = sut.makeAsyncIterator() let received = try await iterator.next() @@ -180,11 +180,11 @@ final class AsyncThrowingReplaySubjectTests: XCTestCase { } } - wait(for: [hasReceivedOneElementExpectation], timeout: 1) + await fulfillment(of: [hasReceivedOneElementExpectation], timeout: 1) sut.send(.failure(expectedError)) - wait(for: [hasFinishedWithFailureExpectation], timeout: 1) + await fulfillment(of: [hasFinishedWithFailureExpectation], timeout: 1) var iterator = sut.makeAsyncIterator() do { @@ -195,7 +195,7 @@ final class AsyncThrowingReplaySubjectTests: XCTestCase { } } - func test_subject_finishes_when_task_is_cancelled() { + func test_subject_finishes_when_task_is_cancelled() async { let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExceptation = expectation(description: "The task has been cancelled") let taskHasFinishedExpectation = expectation(description: "The task has finished") @@ -209,19 +209,19 @@ final class AsyncThrowingReplaySubjectTests: XCTestCase { for try await element in sut { firstElement = element canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) + await fulfillment(of: [hasCancelExceptation], timeout: 5) } XCTAssertEqual(firstElement, 1) taskHasFinishedExpectation.fulfill() } - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task + await fulfillment(of: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task task.cancel() hasCancelExceptation.fulfill() // we can release the lock in the for loop - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished + await fulfillment(of: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished } func test_subject_handles_concurrency() async throws { @@ -254,7 +254,7 @@ final class AsyncThrowingReplaySubjectTests: XCTestCase { return received.sorted() } - await waitForExpectations(timeout: 1) + await fulfillment(of: [canSendExpectation], timeout: 1) // concurrently push values in the sut 1 let task1 = Task { diff --git a/Tests/AsyncSubjets/StreamedTests.swift b/Tests/AsyncSubjets/StreamedTests.swift index 02e6951..3decaf2 100644 --- a/Tests/AsyncSubjets/StreamedTests.swift +++ b/Tests/AsyncSubjets/StreamedTests.swift @@ -6,7 +6,11 @@ // import AsyncExtensions +#if canImport(Combine) import Combine +#elseif canImport(OpenCombine) +import OpenCombine +#endif import XCTest final class StreamedTests: XCTestCase { @@ -20,7 +24,7 @@ final class StreamedTests: XCTestCase { XCTAssertEqual(sut, newValue) } - func test_streamed_projects_in_asyncSequence() { + func test_streamed_projects_in_asyncSequence() async { let firstElementIsReceivedExpectation = expectation(description: "The first element has been received") let fifthElementIsReceivedExpectation = expectation(description: "The fifth element has been received") @@ -40,14 +44,14 @@ final class StreamedTests: XCTestCase { } } - wait(for: [firstElementIsReceivedExpectation], timeout: 1) + await fulfillment(of: [firstElementIsReceivedExpectation], timeout: 1) sut = 1 sut = 2 sut = 3 sut = 4 - wait(for: [fifthElementIsReceivedExpectation], timeout: 1) + await fulfillment(of: [fifthElementIsReceivedExpectation], timeout: 1) task.cancel() } } diff --git a/Tests/Combiners/Merge/AsyncMergeSequenceTests.swift b/Tests/Combiners/Merge/AsyncMergeSequenceTests.swift deleted file mode 100644 index bebc5ef..0000000 --- a/Tests/Combiners/Merge/AsyncMergeSequenceTests.swift +++ /dev/null @@ -1,292 +0,0 @@ -// -// AsyncMergeSequenceTests.swift -// -// -// Created by Thibault Wittemberg on 01/01/2022. -// - -import AsyncExtensions -import XCTest - -private struct TimedAsyncSequence: AsyncSequence, AsyncIteratorProtocol { - typealias Element = Element - typealias AsyncIterator = TimedAsyncSequence - - private let intervalInMills: [UInt64] - private var iterator: Array.Iterator - private var index = 0 - private let indexOfError: Int? - - init(intervalInMills: [UInt64], sequence: [Element], indexOfError: Int? = nil) { - self.intervalInMills = intervalInMills - self.iterator = sequence.makeIterator() - self.indexOfError = indexOfError - } - - mutating func next() async throws -> Element? { - - if let indexOfError = self.indexOfError, self.index == indexOfError { - throw MockError(code: 1) - } - - if self.index < self.intervalInMills.count { - try await Task.sleep(nanoseconds: self.intervalInMills[index] * 1_000_000) - self.index += 1 - } - return self.iterator.next() - } - - func makeAsyncIterator() -> AsyncIterator { - self - } -} - -final class AsyncMergeSequenceTests: XCTestCase { - func testMerge_merges_sequences_according_to_the_timeline_using_asyncSequences() async throws { - // -- 0 ------------------------------- 1000 ----------------------------- 2000 - - // --------------- 500 --------------------------------- 1500 ------------------- - // -- a ----------- d ------------------ b --------------- e --------------- c -- - // - // output should be: a, d, b, e, c - let expectedElements = ["a", "d", "b", "e", "c"] - - let asyncSequence1 = TimedAsyncSequence(intervalInMills: [0, 1000, 1000], sequence: ["a", "b", "c"]) - let asyncSequence2 = TimedAsyncSequence(intervalInMills: [500, 1000], sequence: ["d", "e"]) - - let sut = merge(asyncSequence1, asyncSequence2) - - var receivedElements = [String]() - var iterator = sut.makeAsyncIterator() - while let element = try await iterator.next() { - try await Task.sleep(nanoseconds: 110_000_000) - receivedElements.append(element) - } - - XCTAssertEqual(receivedElements, expectedElements) - - let pastEnd = try await iterator.next() - XCTAssertNil(pastEnd) - } - - func testMerge_merges_four_sequences() async { - let asyncSequence1 = [1, 2, 3, 4, 5] - let asyncSequence2 = [10, 20, 30, 40, 50] - let asyncSequence3 = [100, 200, 300, 400, 500] - let asyncSequence4 = [1000, 2000, 3000, 4000, 5000] - - let expectedElements = asyncSequence1 + asyncSequence2 + asyncSequence3 + asyncSequence4 - - - let sut = merge(asyncSequence1.async, asyncSequence2.async, asyncSequence3.async, asyncSequence4.async) - - var receivedElements = [Int]() - var iterator = sut.makeAsyncIterator() - while let element = await iterator.next() { - receivedElements.append(element) - } - - XCTAssertEqual(receivedElements.sorted(), expectedElements) - - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - } - - func testMerge_merges_sequences_according_to_the_timeline_using_streams() { - let canSend2Expectation = expectation(description: "2 can be sent") - let canSend3Expectation = expectation(description: "3 can be sent") - let canSend4Expectation = expectation(description: "4 can be sent") - let canSend5Expectation = expectation(description: "5 can be sent") - let canSend6Expectation = expectation(description: "6 can be sent") - let canSendFinishExpectation = expectation(description: "finish can be sent") - - let mergedSequenceIsFinisedExpectation = expectation(description: "The merged sequence is finished") - - let stream1 = AsyncCurrentValueSubject(1) - let stream2 = AsyncPassthroughSubject() - let stream3 = AsyncPassthroughSubject() - - let sut = merge(stream1, stream2, stream3) - - Task { - var receivedElements = [Int]() - - for await element in sut { - receivedElements.append(element) - if element == 1 { - canSend2Expectation.fulfill() - } - if element == 2 { - canSend3Expectation.fulfill() - } - if element == 3 { - canSend4Expectation.fulfill() - } - if element == 4 { - canSend5Expectation.fulfill() - } - if element == 5 { - canSend6Expectation.fulfill() - } - - if element == 6 { - canSendFinishExpectation.fulfill() - } - } - XCTAssertEqual(receivedElements, [1, 2, 3, 4, 5, 6]) - mergedSequenceIsFinisedExpectation.fulfill() - } - - wait(for: [canSend2Expectation], timeout: 1) - - stream2.send(2) - wait(for: [canSend3Expectation], timeout: 1) - - stream3.send(3) - wait(for: [canSend4Expectation], timeout: 1) - - stream3.send(4) - wait(for: [canSend5Expectation], timeout: 1) - - stream2.send(5) - wait(for: [canSend6Expectation], timeout: 1) - - stream1.send(6) - - wait(for: [canSendFinishExpectation], timeout: 1) - - stream1.send(Termination.finished) - stream2.send(Termination.finished) - stream3.send(Termination.finished) - - wait(for: [mergedSequenceIsFinisedExpectation], timeout: 1) - } - - func testMerge_returns_empty_sequence_when_all_sequences_are_empty() async { - var receivedResult = [Int]() - - let asyncSequence1 = AsyncEmptySequence() - let asyncSequence2 = AsyncEmptySequence() - let asyncSequence3 = AsyncEmptySequence() - - let sut = merge(asyncSequence1, asyncSequence2, asyncSequence3) - - for await element in sut { - receivedResult.append(element) - } - - XCTAssertTrue(receivedResult.isEmpty) - } - - func testMerge_returns_original_sequence_when_one_sequence_is_empty() async { - let expectedResult = [1, 2, 3] - var receivedResult = [Int]() - - let asyncSequence1 = expectedResult.async - let asyncSequence2 = AsyncEmptySequence() - - let sut = merge(asyncSequence1, asyncSequence2) - - for await element in sut { - receivedResult.append(element) - } - - XCTAssertEqual(receivedResult, expectedResult) - } - - func testMerge_propagates_error() { - let canSend2Expectation = expectation(description: "2 can be sent") - let canSend3Expectation = expectation(description: "3 can be sent") - let mergedSequenceIsFinishedExpectation = expectation(description: "The merged sequence is finished") - - let stream1 = AsyncThrowingCurrentValueSubject(1) - let stream2 = AsyncPassthroughSubject() - - let sut = merge(stream1, stream2) - - Task { - var receivedElements = [Int]() - do { - for try await element in sut { - receivedElements.append(element) - if element == 1 { - canSend2Expectation.fulfill() - } - if element == 2 { - canSend3Expectation.fulfill() - } - } - } catch { - XCTAssertEqual(receivedElements, [1, 2]) - mergedSequenceIsFinishedExpectation.fulfill() - } - } - - wait(for: [canSend2Expectation], timeout: 1) - - stream2.send(2) - wait(for: [canSend3Expectation], timeout: 1) - - stream1.send(.failure(MockError(code: 1))) - - wait(for: [mergedSequenceIsFinishedExpectation], timeout: 1) - } - - func testMerge_finishes_when_task_is_cancelled() { - let canCancelExpectation = expectation(description: "The first element has been emitted") - let hasCancelExceptation = expectation(description: "The task has been cancelled") - let taskHasFinishedExpectation = expectation(description: "The task has finished") - - let asyncSequence1 = TimedAsyncSequence(intervalInMills: [100, 100, 100], sequence: [1, 2, 3]) - let asyncSequence2 = TimedAsyncSequence(intervalInMills: [50, 100, 100, 100], sequence: [6, 7, 8, 9]) - let asyncSequence3 = TimedAsyncSequence(intervalInMills: [1, 399], sequence: [10, 11]) - - let sut = merge(asyncSequence1, asyncSequence2, asyncSequence3) - - let task = Task { - var firstElement: Int? - for try await element in sut { - firstElement = element - canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) - } - XCTAssertEqual(firstElement, 10) - taskHasFinishedExpectation.fulfill() - } - - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task - - task.cancel() - - hasCancelExceptation.fulfill() // we can release the lock in the for loop - - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished - } - - func testMerge_finishes_when_task_is_cancelled_while_waiting_for_an_element() { - let firstElementHasBeenReceivedExpectation = expectation(description: "The first elemenet has been received") - let canIterateExpectation = expectation(description: "We can iterate") - let hasCancelExceptation = expectation(description: "The iteration is cancelled") - - let asyncSequence1 = AsyncCurrentValueSubject(1) - let asyncSequence2 = AsyncPassthroughSubject() - - let sut = merge(asyncSequence1, asyncSequence2) - - let task = Task { - var iterator = sut.makeAsyncIterator() - canIterateExpectation.fulfill() - while let _ = await iterator.next() { - firstElementHasBeenReceivedExpectation.fulfill() - } - hasCancelExceptation.fulfill() - } - - wait(for: [canIterateExpectation], timeout: 1) - - wait(for: [firstElementHasBeenReceivedExpectation], timeout: 1) - - task.cancel() - - wait(for: [hasCancelExceptation], timeout: 1) - } -} diff --git a/Tests/Combiners/WithLatestFrom/AsyncWithLatestFrom2SequenceTests.swift b/Tests/Combiners/WithLatestFrom/AsyncWithLatestFrom2SequenceTests.swift index 8a198da..915e042 100644 --- a/Tests/Combiners/WithLatestFrom/AsyncWithLatestFrom2SequenceTests.swift +++ b/Tests/Combiners/WithLatestFrom/AsyncWithLatestFrom2SequenceTests.swift @@ -60,11 +60,11 @@ final class AsyncWithLatestFrom2SequenceTests: XCTestCase { Task { base.send(0) - wait(for: [baseHasProduced0], timeout: 1.0) + await fulfillment(of: [baseHasProduced0], timeout: 1.0) other1.send("a") - wait(for: [other1HasProducedA], timeout: 1.0) + await fulfillment(of: [other1HasProducedA], timeout: 1.0) other2.send("x") - wait(for: [other2HasProducedX], timeout: 1.0) + await fulfillment(of: [other2HasProducedX], timeout: 1.0) base.send(1) } @@ -73,7 +73,7 @@ final class AsyncWithLatestFrom2SequenceTests: XCTestCase { Task { other2.send("y") - wait(for: [other2HasProducedY], timeout: 1.0) + await fulfillment(of: [other2HasProducedY], timeout: 1.0) base.send(2) } @@ -82,7 +82,7 @@ final class AsyncWithLatestFrom2SequenceTests: XCTestCase { Task { other1.send("b") - wait(for: [other1HasProducedB], timeout: 1.0) + await fulfillment(of: [other1HasProducedB], timeout: 1.0) base.send(3) } @@ -209,11 +209,11 @@ final class AsyncWithLatestFrom2SequenceTests: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 5.0) + await fulfillment(of: [iterated], timeout: 5.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 5.0) + await fulfillment(of: [finished], timeout: 5.0) } } diff --git a/Tests/Combiners/WithLatestFrom/AsyncWithLatestFromSequenceTests.swift b/Tests/Combiners/WithLatestFrom/AsyncWithLatestFromSequenceTests.swift index 2455df1..732673f 100644 --- a/Tests/Combiners/WithLatestFrom/AsyncWithLatestFromSequenceTests.swift +++ b/Tests/Combiners/WithLatestFrom/AsyncWithLatestFromSequenceTests.swift @@ -46,9 +46,9 @@ final class AsyncWithLatestFromSequenceTests: XCTestCase { Task { base.send(0) - wait(for: [baseHasProduced0], timeout: 1.0) + await fulfillment(of: [baseHasProduced0], timeout: 1.0) other.send("a") - wait(for: [otherHasProducedA], timeout: 1.0) + await fulfillment(of: [otherHasProducedA], timeout: 1.0) base.send(1) } @@ -63,7 +63,7 @@ final class AsyncWithLatestFromSequenceTests: XCTestCase { Task { other.send("b") other.send("c") - wait(for: [otherHasProducedC], timeout: 1.0) + await fulfillment(of: [otherHasProducedC], timeout: 1.0) base.send(3) } @@ -151,11 +151,11 @@ final class AsyncWithLatestFromSequenceTests: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/Combiners/Zip/AsyncZipSequenceTests.swift b/Tests/Combiners/Zip/AsyncZipSequenceTests.swift deleted file mode 100644 index 701bd48..0000000 --- a/Tests/Combiners/Zip/AsyncZipSequenceTests.swift +++ /dev/null @@ -1,415 +0,0 @@ -// -// AsyncZipSequenceTests.swift -// -// -// Created by Thibault Wittemberg on 14/01/2022. -// - -@testable import AsyncExtensions -import XCTest - -private struct TimedAsyncSequence: AsyncSequence, AsyncIteratorProtocol { - typealias Element = Element - typealias AsyncIterator = TimedAsyncSequence - - private let intervalInMills: UInt64 - private var iterator: Array.Iterator - - init(intervalInMills: UInt64, sequence: [Element]) { - self.intervalInMills = intervalInMills - self.iterator = sequence.makeIterator() - } - - mutating func next() async -> Element? { - try? await Task.sleep(nanoseconds: self.intervalInMills * 1_000_000) - return self.iterator.next() - } - - func makeAsyncIterator() -> AsyncIterator { - self - } -} - -final class AsyncZipSequenceTests: XCTestCase { - func testZip2_respects_chronology_and_ends_when_first_sequence_ends() async { - let asyncSeq1 = TimedAsyncSequence(intervalInMills: 50, sequence: [1, 2, 3, 4]) - let asyncSeq2 = TimedAsyncSequence(intervalInMills: 10, sequence: ["6", "7", "8", "9", "10"]) - - let sut = zip(asyncSeq1, asyncSeq2) - - var receivedElements = [(Int, String)]() - - for await element in sut { - receivedElements.append(element) - } - - XCTAssertEqual(receivedElements.map { $0.0 }, [1, 2, 3, 4]) - XCTAssertEqual(receivedElements.map { $0.1 }, ["6", "7", "8", "9"]) - } - - func testZip2_respects_chronology_and_ends_when_second_sequence_ends() async { - let asyncSeq1 = TimedAsyncSequence(intervalInMills: 50, sequence: [1, 2, 3, 4, 5]) - let asyncSeq2 = TimedAsyncSequence(intervalInMills: 10, sequence: ["6", "7", "8"]) - - let sut = zip(asyncSeq1, asyncSeq2) - - var receivedElements = [(Int, String)]() - - for await element in sut { - receivedElements.append(element) - } - - XCTAssertEqual(receivedElements.map { $0.0 }, [1, 2, 3]) - XCTAssertEqual(receivedElements.map { $0.1 }, ["6", "7", "8"]) - } - - func testZip2_respects_returns_nil_pastEnd() async { - let asyncSeq1 = AsyncLazySequence([1, 2, 3]) - let asyncSeq2 = AsyncLazySequence(["1", "2", "3"]) - - let sut = zip(asyncSeq1, asyncSeq2) - - var receivedElements = [(Int, String)]() - let iterator = sut.makeAsyncIterator() - - while let element = await iterator.next() { - receivedElements.append(element) - } - - XCTAssertEqual(receivedElements.map { $0.0 }, [1, 2, 3]) - XCTAssertEqual(receivedElements.map { $0.1 }, ["1", "2", "3"]) - - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - } - - func testZip2_propagates_error_when_first_fails() async throws { - let mockError = MockError(code: Int.random(in: 0...100)) - - let asyncSeq1 = AsyncFailSequence( mockError) - let asyncSeq2 = TimedAsyncSequence(intervalInMills: 50, sequence: [1, 2, 3, 4]) - - let sut = zip(asyncSeq1, asyncSeq2) - let iterator = sut.makeAsyncIterator() - - do { - while let element = try await iterator.next() { - print(element) - } - XCTFail("The zipped sequence should fail") - } catch { - XCTAssertEqual(error as? MockError, mockError) - } - - let pastFail = try await iterator.next() - XCTAssertNil(pastFail) - } - - func testZip2_propagates_error_when_second_fails() async throws { - let mockError = MockError(code: Int.random(in: 0...100)) - - let asyncSeq1 = TimedAsyncSequence(intervalInMills: 50, sequence: [1, 2, 3, 4]) - let asyncSeq2 = AsyncFailSequence( mockError) - - let sut = zip(asyncSeq1, asyncSeq2) - let iterator = sut.makeAsyncIterator() - - do { - while let _ = try await iterator.next() {} - XCTFail("The zipped sequence should fail") - } catch { - XCTAssertEqual(error as? MockError, mockError) - } - - let pastFail = try await iterator.next() - XCTAssertNil(pastFail) - } - - func testZip2_finishes_when_task_is_cancelled() { - let canCancelExpectation = expectation(description: "The first element has been emitted") - let hasCancelExceptation = expectation(description: "The task has been cancelled") - let taskHasFinishedExpectation = expectation(description: "The task has finished") - - let asyncSeq1 = AsyncLazySequence([1, 2, 3]) - let asyncSeq2 = AsyncLazySequence(["1", "2", "3"]) - - let sut = zip(asyncSeq1, asyncSeq2) - - let task = Task { - var firstElement: (Int, String)? - for try await element in sut { - firstElement = element - canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) - } - XCTAssertEqual(firstElement!.0, 1) - XCTAssertEqual(firstElement!.1, "1") - taskHasFinishedExpectation.fulfill() - } - - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task - - task.cancel() - - hasCancelExceptation.fulfill() // we can release the lock in the for loop - - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished - } -} - -extension AsyncZipSequenceTests { - func testZip3_respects_chronology_and_ends_when_first_sequence_ends() async throws { - let asyncSeq1 = TimedAsyncSequence(intervalInMills: 50, sequence: [1, 2, 3]) - let asyncSeq2 = TimedAsyncSequence(intervalInMills: 10, sequence: ["6", "7", "8", "9", "10"]) - let asyncSeq3 = TimedAsyncSequence(intervalInMills: 30, sequence: [true, false, true, false, true]) - - let sut = zip(asyncSeq1, asyncSeq2, asyncSeq3) - - var receivedElements = [(Int, String, Bool)]() - - for try await element in sut { - receivedElements.append(element) - } - - XCTAssertEqual(receivedElements.map { $0.0 }, [1, 2, 3]) - XCTAssertEqual(receivedElements.map { $0.1 }, ["6", "7", "8"]) - XCTAssertEqual(receivedElements.map { $0.2 }, [true, false, true]) - } - - func testZip3_respects_chronology_and_ends_when_second_sequence_ends() async throws { - let asyncSeq1 = TimedAsyncSequence(intervalInMills: 50, sequence: [1, 2, 3, 4, 5]) - let asyncSeq2 = TimedAsyncSequence(intervalInMills: 10, sequence: ["6", "7", "8"]) - let asyncSeq3 = TimedAsyncSequence(intervalInMills: 30, sequence: [true, false, true, false, true]) - - let sut = zip(asyncSeq1, asyncSeq2, asyncSeq3) - - var receivedElements = [(Int, String, Bool)]() - - for try await element in sut { - receivedElements.append(element) - } - - XCTAssertEqual(receivedElements.map { $0.0 }, [1, 2, 3]) - XCTAssertEqual(receivedElements.map { $0.1 }, ["6", "7", "8"]) - XCTAssertEqual(receivedElements.map { $0.2 }, [true, false, true]) - } - - func testZip3_respects_chronology_and_ends_when_third_sequence_ends() async throws { - let asyncSeq1 = TimedAsyncSequence(intervalInMills: 50, sequence: [1, 2, 3, 4, 5]) - let asyncSeq2 = TimedAsyncSequence(intervalInMills: 10, sequence: ["6", "7", "8", "9", "10"]) - let asyncSeq3 = TimedAsyncSequence(intervalInMills: 30, sequence: [true, false, true]) - - let sut = zip(asyncSeq1, asyncSeq2, asyncSeq3) - - var receivedElements = [(Int, String, Bool)]() - - for try await element in sut { - receivedElements.append(element) - } - - XCTAssertEqual(receivedElements.map { $0.0 }, [1, 2, 3]) - XCTAssertEqual(receivedElements.map { $0.1 }, ["6", "7", "8"]) - XCTAssertEqual(receivedElements.map { $0.2 }, [true, false, true]) - } - - func testZip3_respects_returns_nil_pastEnd() async { - let asyncSeq1 = AsyncLazySequence([1, 2, 3]) - let asyncSeq2 = AsyncLazySequence(["1", "2", "3"]) - let asyncSeq3 = AsyncLazySequence([true, false, true]) - - let sut = zip(asyncSeq1, asyncSeq2, asyncSeq3) - - var receivedElements = [(Int, String, Bool)]() - let iterator = sut.makeAsyncIterator() - - while let element = await iterator.next() { - receivedElements.append(element) - } - - XCTAssertEqual(receivedElements.map { $0.0 }, [1, 2, 3]) - XCTAssertEqual(receivedElements.map { $0.1 }, ["1", "2", "3"]) - XCTAssertEqual(receivedElements.map { $0.2 }, [true, false, true]) - - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - } - - func testZip3_propagates_error_when_first_fails() async throws { - let mockError = MockError(code: Int.random(in: 0...100)) - - let asyncSeq1 = AsyncFailSequence( mockError) - let asyncSeq2 = TimedAsyncSequence(intervalInMills: 50, sequence: [1, 2, 3, 4]) - let asyncSeq3 = TimedAsyncSequence(intervalInMills: 5, sequence: ["1", "2", "3", "4"]) - - let sut = zip(asyncSeq1, asyncSeq2, asyncSeq3) - let iterator = sut.makeAsyncIterator() - - do { - while let _ = try await iterator.next() {} - XCTFail("The zipped sequence should fail") - } catch { - XCTAssertEqual(error as? MockError, mockError) - } - - let pastFail = try await iterator.next() - XCTAssertNil(pastFail) - } - - func testZip3_propagates_error_when_second_fails() async throws { - let mockError = MockError(code: Int.random(in: 0...100)) - - let asyncSeq1 = TimedAsyncSequence(intervalInMills: 50, sequence: [1, 2, 3, 4]) - let asyncSeq2 = AsyncFailSequence( mockError) - let asyncSeq3 = TimedAsyncSequence(intervalInMills: 5, sequence: ["1", "2", "3", "4"]) - - let sut = zip(asyncSeq1, asyncSeq2, asyncSeq3) - let iterator = sut.makeAsyncIterator() - - do { - while let _ = try await iterator.next() {} - XCTFail("The zipped sequence should fail") - } catch { - XCTAssertEqual(error as? MockError, mockError) - } - - let pastFail = try await iterator.next() - XCTAssertNil(pastFail) - } - - func testZip3_propagates_error_when_third_fails() async throws { - let mockError = MockError(code: Int.random(in: 0...100)) - - let asyncSeq1 = TimedAsyncSequence(intervalInMills: 50, sequence: [1, 2, 3, 4]) - let asyncSeq2 = TimedAsyncSequence(intervalInMills: 5, sequence: ["1", "2", "3", "4"]) - let asyncSeq3 = AsyncFailSequence( mockError) - - let sut = zip(asyncSeq1, asyncSeq2, asyncSeq3) - let iterator = sut.makeAsyncIterator() - - do { - while let _ = try await iterator.next() {} - XCTFail("The zipped sequence should fail") - } catch { - XCTAssertEqual(error as? MockError, mockError) - } - - let pastFail = try await iterator.next() - XCTAssertNil(pastFail) - } - - func testZip3_finishes_when_task_is_cancelled() { - let canCancelExpectation = expectation(description: "The first element has been emitted") - let hasCancelExceptation = expectation(description: "The task has been cancelled") - let taskHasFinishedExpectation = expectation(description: "The task has finished") - - let asyncSeq1 = AsyncLazySequence([1, 2, 3]) - let asyncSeq2 = AsyncLazySequence(["1", "2", "3"]) - let asyncSeq3 = AsyncLazySequence([true, false, true]) - - let sut = zip(asyncSeq1, asyncSeq2, asyncSeq3) - - let task = Task { - var firstElement: (Int, String, Bool)? - for try await element in sut { - firstElement = element - canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) - } - XCTAssertEqual(firstElement!.0, 1) // the AsyncSequence is cancelled having only emitted the first element - XCTAssertEqual(firstElement!.1, "1") - XCTAssertEqual(firstElement!.2, true) - taskHasFinishedExpectation.fulfill() - } - - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task - - task.cancel() - - hasCancelExceptation.fulfill() // we can release the lock in the for loop - - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished - } -} - -extension AsyncZipSequenceTests { - func testZip_respects_chronology_and_ends_when_any_sequence_ends() async { - let asyncSeq1 = TimedAsyncSequence(intervalInMills: 50, sequence: [1, 2, 3, 4, 5]) - let asyncSeq2 = TimedAsyncSequence(intervalInMills: 10, sequence: [1, 2, 3]) - let asyncSeq3 = TimedAsyncSequence(intervalInMills: 30, sequence: [1, 2, 3, 4, 5]) - let asyncSeq4 = TimedAsyncSequence(intervalInMills: 5, sequence: [1, 2, 3]) - let asyncSeq5 = TimedAsyncSequence(intervalInMills: 20, sequence: [1, 2, 3, 4, 5]) - - let sut = zip(asyncSeq1, asyncSeq2, asyncSeq3, asyncSeq4, asyncSeq5) - - var receivedElements = [[Int]]() - - let iterator = sut.makeAsyncIterator() - while let element = await iterator.next() { - receivedElements.append(element) - } - - XCTAssertEqual(receivedElements.count, 3) - XCTAssertEqual(receivedElements[0], [1, 1, 1, 1, 1]) - XCTAssertEqual(receivedElements[1], [2, 2, 2, 2, 2]) - XCTAssertEqual(receivedElements[2], [3, 3, 3, 3, 3]) - - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - } - - func testZip_propagates_error() async throws { - let mockError = MockError(code: Int.random(in: 0...100)) - - let asyncSeq1 = TimedAsyncSequence(intervalInMills: 5, sequence: [1, 2, 3, 4]).eraseToAnyAsyncSequence() - let asyncSeq2 = TimedAsyncSequence(intervalInMills: 10, sequence: [1, 2, 3, 4]).eraseToAnyAsyncSequence() - let asyncSeq3 = AsyncFailSequence( mockError).eraseToAnyAsyncSequence() - let asyncSeq4 = TimedAsyncSequence(intervalInMills: 20, sequence: [1, 2, 3, 4]).eraseToAnyAsyncSequence() - let asyncSeq5 = TimedAsyncSequence(intervalInMills: 15, sequence: [1, 2, 3, 4]).eraseToAnyAsyncSequence() - - let sut = zip(asyncSeq1, asyncSeq2, asyncSeq3, asyncSeq4, asyncSeq5) - let iterator = sut.makeAsyncIterator() - - do { - while let _ = try await iterator.next() {} - XCTFail("The zipped sequence should fail") - } catch { - XCTAssertEqual(error as? MockError, mockError) - } - - let pastFail = try await iterator.next() - XCTAssertNil(pastFail) - } - - func testZip_finishes_when_task_is_cancelled() { - let canCancelExpectation = expectation(description: "The first element has been emitted") - let hasCancelExceptation = expectation(description: "The task has been cancelled") - let taskHasFinishedExpectation = expectation(description: "The task has finished") - - let asyncSeq1 = AsyncLazySequence([1, 2, 3]) - let asyncSeq2 = AsyncLazySequence([1, 2, 3]) - let asyncSeq3 = AsyncLazySequence([1, 2, 3]) - let asyncSeq4 = AsyncLazySequence([1, 2, 3]) - let asyncSeq5 = AsyncLazySequence([1, 2, 3]) - - let sut = zip(asyncSeq1, asyncSeq2, asyncSeq3, asyncSeq4, asyncSeq5) - - let task = Task { - var firstElement: [Int]? - for await element in sut { - firstElement = element - canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) - } - XCTAssertEqual(firstElement!, [1, 1, 1, 1, 1]) - taskHasFinishedExpectation.fulfill() - } - - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task - - task.cancel() - - hasCancelExceptation.fulfill() // we can release the lock in the for loop - - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished - } -} diff --git a/Tests/Creators/AsyncFailSequenceTests.swift b/Tests/Creators/AsyncFailSequenceTests.swift index 24790fb..7bd55c4 100644 --- a/Tests/Creators/AsyncFailSequenceTests.swift +++ b/Tests/Creators/AsyncFailSequenceTests.swift @@ -32,7 +32,7 @@ final class AsyncFailSequenceTests: XCTestCase { XCTAssertTrue(receivedResult.isEmpty) } - func test_AsyncFailSequence_returns_an_asyncSequence_that_finishes_without_error_when_task_is_cancelled() { + func test_AsyncFailSequence_returns_an_asyncSequence_that_finishes_without_error_when_task_is_cancelled() async { let taskHasBeenCancelledExpectation = expectation(description: "The task has been cancelled") let sequenceHasFinishedExpectation = expectation(description: "The async sequence has finished") @@ -41,7 +41,7 @@ final class AsyncFailSequenceTests: XCTestCase { let task = Task { do { var iterator = failSequence.makeAsyncIterator() - wait(for: [taskHasBeenCancelledExpectation], timeout: 1) + await fulfillment(of: [taskHasBeenCancelledExpectation], timeout: 1) while let _ = try await iterator.next() { XCTFail("The AsyncSequence should not output elements") } @@ -56,6 +56,6 @@ final class AsyncFailSequenceTests: XCTestCase { taskHasBeenCancelledExpectation.fulfill() - wait(for: [sequenceHasFinishedExpectation], timeout: 1) + await fulfillment(of: [sequenceHasFinishedExpectation], timeout: 1) } } diff --git a/Tests/Creators/AsyncJustSequenceTests.swift b/Tests/Creators/AsyncJustSequenceTests.swift index 407fb1a..e96d9ff 100644 --- a/Tests/Creators/AsyncJustSequenceTests.swift +++ b/Tests/Creators/AsyncJustSequenceTests.swift @@ -22,14 +22,14 @@ final class AsyncJustSequenceTests: XCTestCase { XCTAssertEqual(receivedResult, [element]) } - func test_AsyncJustSequence_returns_an_asyncSequence_that_finishes_without_elements_when_task_is_cancelled() { + func test_AsyncJustSequence_returns_an_asyncSequence_that_finishes_without_elements_when_task_is_cancelled() async { let hasCancelledExpectation = expectation(description: "The task has been cancelled") let hasFinishedExpectation = expectation(description: "The AsyncSequence has finished") let justSequence = AsyncJustSequence(1) let task = Task { - wait(for: [hasCancelledExpectation], timeout: 1) + await fulfillment(of: [hasCancelledExpectation], timeout: 1) for await _ in justSequence { XCTFail("The AsyncSequence should not output elements") } @@ -40,6 +40,6 @@ final class AsyncJustSequenceTests: XCTestCase { hasCancelledExpectation.fulfill() - wait(for: [hasFinishedExpectation], timeout: 1) + await fulfillment(of: [hasFinishedExpectation], timeout: 1) } } diff --git a/Tests/Creators/AsyncLazySequenceTests.swift b/Tests/Creators/AsyncLazySequenceTests.swift deleted file mode 100644 index f164fa9..0000000 --- a/Tests/Creators/AsyncLazySequenceTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// AsyncLazySequenceTests.swift -// -// -// Created by Thibault Wittemberg on 02/01/2022. -// - -import AsyncExtensions -import XCTest - -final class AsyncLazySequenceTests: XCTestCase { - func test_AsyncLazySequence_returns_original_sequence() async { - var receivedResult = [Int]() - - let sequence = [1, 2, 3, 4, 5] - - let sut = AsyncLazySequence(sequence) - - for await element in sut { - receivedResult.append(element) - } - - XCTAssertEqual(receivedResult, sequence) - } - - func test_AsyncLazySequence_returns_an_asyncSequence_that_finishes_when_task_is_cancelled() { - let canCancelExpectation = expectation(description: "The first element has been emitted") - let hasCancelExceptation = expectation(description: "The task has been cancelled") - - let sequence = (0...1_000_000) - - let sut = AsyncLazySequence(sequence) - - let task = Task { - var firstElement: Int? - for await element in sut { - firstElement = element - canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) - } - XCTAssertEqual(firstElement!, 0) // the AsyncSequence is cancelled having only emitted the first element - } - - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task - - task.cancel() - - hasCancelExceptation.fulfill() // we can release the lock in the for loop - } -} diff --git a/Tests/Creators/AsyncStream+PipeTests.swift b/Tests/Creators/AsyncStream+PipeTests.swift index 1962dfd..f0d827f 100644 --- a/Tests/Creators/AsyncStream+PipeTests.swift +++ b/Tests/Creators/AsyncStream+PipeTests.swift @@ -9,7 +9,7 @@ import AsyncExtensions import XCTest final class AsyncStream_PipeTests: XCTestCase { - func test_pipe_produces_stream_input_and_output() { + func test_pipe_produces_stream_input_and_output() async { let finished = expectation(description: "The stream has finished") // Given @@ -30,10 +30,10 @@ final class AsyncStream_PipeTests: XCTestCase { input.yield(2) input.finish() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } - func test_pipe_produces_stream_input_and_output_that_can_throw() { + func test_pipe_produces_stream_input_and_output_that_can_throw() async { let finished = expectation(description: "The stream has finished") // Given @@ -58,6 +58,6 @@ final class AsyncStream_PipeTests: XCTestCase { input.yield(2) input.yield(with: .failure(MockError(code: 1701))) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/Creators/AsyncThrowingJustSequenceTests.swift b/Tests/Creators/AsyncThrowingJustSequenceTests.swift index aa07089..da71bda 100644 --- a/Tests/Creators/AsyncThrowingJustSequenceTests.swift +++ b/Tests/Creators/AsyncThrowingJustSequenceTests.swift @@ -47,14 +47,14 @@ final class AsyncThrowingJustSequenceTests: XCTestCase { } } - func test_AsyncThrowingJustSequence_returns_an_asyncSequence_that_finishes_without_elements_when_task_is_cancelled() { + func test_AsyncThrowingJustSequence_returns_an_asyncSequence_that_finishes_without_elements_when_task_is_cancelled() async { let hasCancelledExpectation = expectation(description: "The task has been cancelled") let hasFinishedExpectation = expectation(description: "The AsyncSequence has finished") let justSequence = AsyncThrowingJustSequence(1) let task = Task { - wait(for: [hasCancelledExpectation], timeout: 1) + await fulfillment(of: [hasCancelledExpectation], timeout: 1) for try await _ in justSequence { XCTFail("The AsyncSequence should not output elements") } @@ -65,6 +65,6 @@ final class AsyncThrowingJustSequenceTests: XCTestCase { hasCancelledExpectation.fulfill() - wait(for: [hasFinishedExpectation], timeout: 1) + await fulfillment(of: [hasFinishedExpectation], timeout: 1) } } diff --git a/Tests/Creators/AsyncTimerSequenceTests.swift b/Tests/Creators/AsyncTimerSequenceTests.swift index ca9ff5c..5b7e3d2 100644 --- a/Tests/Creators/AsyncTimerSequenceTests.swift +++ b/Tests/Creators/AsyncTimerSequenceTests.swift @@ -9,7 +9,7 @@ import AsyncExtensions import XCTest final class AsyncTimerSequenceTests: XCTestCase { - func testTimer_finishes_when_task_is_cancelled() { + func testTimer_finishes_when_task_is_cancelled() async { let canCancelExpectation = expectation(description: "the timer can be cancelled") let asyncSequenceHasFinishedExpectation = expectation(description: "The async sequence has finished") @@ -26,10 +26,10 @@ final class AsyncTimerSequenceTests: XCTestCase { asyncSequenceHasFinishedExpectation.fulfill() } - wait(for: [canCancelExpectation], timeout: 5) + await fulfillment(of: [canCancelExpectation], timeout: 5) task.cancel() - wait(for: [asyncSequenceHasFinishedExpectation], timeout: 5) + await fulfillment(of: [asyncSequenceHasFinishedExpectation], timeout: 5) } } diff --git a/Tests/Operators/AsyncHandleEventsSequenceTests.swift b/Tests/Operators/AsyncHandleEventsSequenceTests.swift index 52b90c3..38b2466 100644 --- a/Tests/Operators/AsyncHandleEventsSequenceTests.swift +++ b/Tests/Operators/AsyncHandleEventsSequenceTests.swift @@ -28,7 +28,7 @@ final class AsyncHandleEventsSequenceTests: XCTestCase { XCTAssertEqual(received.criticalState, ["start", "1", "2", "3", "4", "5", "finish finished"]) } - func test_iteration_calls_onCancel_when_task_is_cancelled() { + func test_iteration_calls_onCancel_when_task_is_cancelled() async { let firstElementHasBeenReceivedExpectation = expectation(description: "First element has been emitted") let taskHasBeenCancelledExpectation = expectation(description: "The task has been cancelled") let onCancelHasBeenCalledExpectation = expectation(description: "OnCancel has been called") @@ -53,17 +53,17 @@ final class AsyncHandleEventsSequenceTests: XCTestCase { firstElementHasBeenReceivedExpectation.fulfill() } - wait(for: [taskHasBeenCancelledExpectation], timeout: 1) + await fulfillment(of: [taskHasBeenCancelledExpectation], timeout: 1) } } - wait(for: [firstElementHasBeenReceivedExpectation], timeout: 1) + await fulfillment(of: [firstElementHasBeenReceivedExpectation], timeout: 1) task.cancel() taskHasBeenCancelledExpectation.fulfill() - wait(for: [onCancelHasBeenCalledExpectation], timeout: 1) + await fulfillment(of: [onCancelHasBeenCalledExpectation], timeout: 1) XCTAssertEqual(received.criticalState, ["start", "1", "cancelled"]) } @@ -95,10 +95,10 @@ final class AsyncHandleEventsSequenceTests: XCTestCase { XCTAssertEqual(error as? MockError, expectedError) } - await waitForExpectations(timeout: 1) + await fulfillment(of: [onFinishHasBeenCalledExpectation], timeout: 1) } - func test_iteration_finishes_when_task_is_cancelled() { + func test_iteration_finishes_when_task_is_cancelled() async { let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExceptation = expectation(description: "The task has been cancelled") let taskHasFinishedExpectation = expectation(description: "The task has finished") @@ -112,18 +112,18 @@ final class AsyncHandleEventsSequenceTests: XCTestCase { for try await element in handledSequence { firstElement = element canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) + await fulfillment(of: [hasCancelExceptation], timeout: 5) } XCTAssertEqual(firstElement, 0) taskHasFinishedExpectation.fulfill() } - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task + await fulfillment(of: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task task.cancel() hasCancelExceptation.fulfill() // we can release the lock in the for loop - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished + await fulfillment(of: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished } } diff --git a/Tests/Operators/AsyncMulticastSequenceTests.swift b/Tests/Operators/AsyncMulticastSequenceTests.swift index a77e4cf..c3af0af 100644 --- a/Tests/Operators/AsyncMulticastSequenceTests.swift +++ b/Tests/Operators/AsyncMulticastSequenceTests.swift @@ -8,6 +8,27 @@ import AsyncExtensions import XCTest +private struct SpyAsyncSequenceForOnNextCall: AsyncSequence { + typealias Element = Element + typealias AsyncIterator = Iterator + + let onNext: () -> Void + + func makeAsyncIterator() -> AsyncIterator { + Iterator(onNext: self.onNext) + } + + struct Iterator: AsyncIteratorProtocol { + let onNext: () -> Void + + func next() async throws -> Element? { + self.onNext() + try await Task.sleep(nanoseconds: 100_000_000_000) + return nil + } + } +} + private class SpyAsyncSequenceForNumberOfIterators: AsyncSequence { typealias Element = Element typealias AsyncIterator = Iterator @@ -40,7 +61,7 @@ private class SpyAsyncSequenceForNumberOfIterators: AsyncSequence { } final class AsyncMulticastSequenceTests: XCTestCase { - func test_multiple_loops_receive_elements_from_single_baseIterator() { + func test_multiple_loops_receive_elements_from_single_baseIterator() async { let taskHaveIterators = expectation(description: "All tasks have their iterator") taskHaveIterators.expectedFulfillmentCount = 2 @@ -73,16 +94,16 @@ final class AsyncMulticastSequenceTests: XCTestCase { tasksHaveFinishedExpectation.fulfill() } - wait(for: [taskHaveIterators], timeout: 1) + await fulfillment(of: [taskHaveIterators], timeout: 1) sut.connect() - wait(for: [tasksHaveFinishedExpectation], timeout: 1) + await fulfillment(of: [tasksHaveFinishedExpectation], timeout: 1) XCTAssertEqual(spyUpstreamSequence.numberOfIterators, 1) } - func test_multiple_loops_uses_provided_stream() { + func test_multiple_loops_uses_provided_stream() async { let taskHaveIterators = expectation(description: "All tasks have their iterator") taskHaveIterators.expectedFulfillmentCount = 3 @@ -126,11 +147,11 @@ final class AsyncMulticastSequenceTests: XCTestCase { tasksHaveFinishedExpectation.fulfill() } - wait(for: [taskHaveIterators], timeout: 1) + await fulfillment(of: [taskHaveIterators], timeout: 1) sut.connect() - wait(for: [tasksHaveFinishedExpectation], timeout: 1) + await fulfillment(of: [tasksHaveFinishedExpectation], timeout: 1) XCTAssertEqual(spyUpstreamSequence.numberOfIterators, 1) } @@ -156,4 +177,46 @@ final class AsyncMulticastSequenceTests: XCTestCase { XCTAssertEqual(error as? MockError, expectedError) } } + + func test_multicast_finishes_when_task_is_cancelled() async { + let taskHasFinishedExpectation = expectation(description: "Task has finished") + + let stream = AsyncThrowingPassthroughSubject() + let sut = [1, 2, 3, 4, 5] + .async + .multicast(stream) + .autoconnect() + + Task { + for try await _ in sut {} + taskHasFinishedExpectation.fulfill() + }.cancel() + + await fulfillment(of: [taskHasFinishedExpectation], timeout: 1) + } + + func test_multicast_finishes_when_task_is_cancelled_while_waiting_for_next() async { + let canCancelExpectation = expectation(description: "the task can be cancelled") + let taskHasFinishedExpectation = expectation(description: "Task has finished") + + let spyAsyncSequence = SpyAsyncSequenceForOnNextCall { + canCancelExpectation.fulfill() + } + + let stream = AsyncThrowingPassthroughSubject() + let sut = spyAsyncSequence + .multicast(stream) + .autoconnect() + + let task = Task { + for try await _ in sut {} + taskHasFinishedExpectation.fulfill() + } + + await fulfillment(of: [canCancelExpectation], timeout: 1) + + task.cancel() + + await fulfillment(of: [taskHasFinishedExpectation], timeout: 1) + } } diff --git a/Tests/Operators/AsyncPrependSequenceTests.swift b/Tests/Operators/AsyncPrependSequenceTests.swift index e520e06..6e1836d 100644 --- a/Tests/Operators/AsyncPrependSequenceTests.swift +++ b/Tests/Operators/AsyncPrependSequenceTests.swift @@ -5,6 +5,7 @@ // Created by Thibault Wittemberg on 01/01/2022. // +import AsyncAlgorithms import AsyncExtensions import XCTest @@ -23,7 +24,7 @@ final class AsyncPrependSequenceTests: XCTestCase { XCTAssertEqual(receivedResult, expectedResult) } - func testPrepend_finishes_when_task_is_cancelled() { + func testPrepend_finishes_when_task_is_cancelled() async { let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExceptation = expectation(description: "The task has been cancelled") let taskHasFinishedExpectation = expectation(description: "The task has finished") @@ -37,18 +38,18 @@ final class AsyncPrependSequenceTests: XCTestCase { for try await element in prependedSequence { firstElement = element canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) + await fulfillment(of: [hasCancelExceptation], timeout: 5) } XCTAssertEqual(firstElement, 0) taskHasFinishedExpectation.fulfill() } - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task + await fulfillment(of: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task task.cancel() hasCancelExceptation.fulfill() // we can release the lock in the for loop - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished + await fulfillment(of: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished } } diff --git a/Tests/Operators/AsyncScanSequenceTests.swift b/Tests/Operators/AsyncScanSequenceTests.swift index 2833086..ae76661 100644 --- a/Tests/Operators/AsyncScanSequenceTests.swift +++ b/Tests/Operators/AsyncScanSequenceTests.swift @@ -26,7 +26,7 @@ final class AsyncScanSequenceTests: XCTestCase { XCTAssertEqual(receivedResult, expectedResult) } - func testScan_finishes_when_task_is_cancelled() { + func testScan_finishes_when_task_is_cancelled() async { let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExceptation = expectation(description: "The task has been cancelled") let taskHasFinishedExpectation = expectation(description: "The task has finished") @@ -43,18 +43,18 @@ final class AsyncScanSequenceTests: XCTestCase { for try await element in sut { firstElement = element canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) + await fulfillment(of: [hasCancelExceptation], timeout: 5) } XCTAssertEqual(firstElement, "1") taskHasFinishedExpectation.fulfill() } - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task + await fulfillment(of: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task task.cancel() hasCancelExceptation.fulfill() // we can release the lock in the for loop - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished + await fulfillment(of: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished } } diff --git a/Tests/Operators/AsyncSequence+AssignTests.swift b/Tests/Operators/AsyncSequence+AssignTests.swift index d738d14..03688f2 100644 --- a/Tests/Operators/AsyncSequence+AssignTests.swift +++ b/Tests/Operators/AsyncSequence+AssignTests.swift @@ -5,6 +5,7 @@ // Created by Thibault Wittemberg on 02/02/2022. // +import AsyncAlgorithms import AsyncExtensions import XCTest @@ -21,7 +22,7 @@ private class Root { final class AsyncSequence_AssignTests: XCTestCase { func testAssign_sets_elements_on_the_root() async throws { let root = Root() - let sut = AsyncLazySequence(["1", "2", "3"]) + let sut = ["1", "2", "3"].async try await sut.assign(to: \.property, on: root) XCTAssertEqual(root.successiveValues, ["1", "2", "3"]) } diff --git a/Tests/Operators/AsyncSequence+FlatMapLatestTests.swift b/Tests/Operators/AsyncSequence+FlatMapLatestTests.swift index a968baf..f515aec 100644 --- a/Tests/Operators/AsyncSequence+FlatMapLatestTests.swift +++ b/Tests/Operators/AsyncSequence+FlatMapLatestTests.swift @@ -5,6 +5,7 @@ // Created by Thibault Wittemberg on 10/01/2022. // +import AsyncAlgorithms @testable import AsyncExtensions import XCTest @@ -166,10 +167,10 @@ final class AsyncSequence_FlatMapLatestTests: XCTestCase { func testFlatMapLatest_propagates_errors() async { let expectedError = MockError(code: Int.random(in: 0...100)) - let sut = AsyncLazySequence([1, 2]) + let sut = [1, 2].async .flatMapLatest { element -> AnyAsyncSequence in if element == 1 { - return AsyncLazySequence([1]).eraseToAnyAsyncSequence() + return [1].async.eraseToAnyAsyncSequence() } return AsyncFailSequence(expectedError).eraseToAnyAsyncSequence() @@ -183,7 +184,7 @@ final class AsyncSequence_FlatMapLatestTests: XCTestCase { } } - func testFlatMapLatest_finishes_when_task_is_cancelled_after_switched() { + func testFlatMapLatest_finishes_when_task_is_cancelled_after_switched() async { let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExceptation = expectation(description: "The task has been cancelled") let taskHasFinishedExpectation = expectation(description: "The task has finished") @@ -199,19 +200,19 @@ final class AsyncSequence_FlatMapLatestTests: XCTestCase { for try await element in sut { firstElement = element canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) + await fulfillment(of: [hasCancelExceptation], timeout: 5) } XCTAssertEqual(firstElement, 3) taskHasFinishedExpectation.fulfill() } - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task + await fulfillment(of: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task task.cancel() hasCancelExceptation.fulfill() // we can release the lock in the for loop - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished + await fulfillment(of: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished } func testFlatMapLatest_switches_to_latest_element() async throws { diff --git a/Tests/Operators/AsyncSequence+ShareTests.swift b/Tests/Operators/AsyncSequence+ShareTests.swift index 51b18d9..40d4502 100644 --- a/Tests/Operators/AsyncSequence+ShareTests.swift +++ b/Tests/Operators/AsyncSequence+ShareTests.swift @@ -47,7 +47,7 @@ private struct LongAsyncSequence: AsyncSequence, AsyncIteratorProtocol throw MockError(code: 0) } return self.elements.next() - } onCancel: {[onCancel] in + } onCancel: { [onCancel] in onCancel() } } @@ -58,7 +58,7 @@ private struct LongAsyncSequence: AsyncSequence, AsyncIteratorProtocol } final class AsyncSequence_ShareTests: XCTestCase { - func test_share_multicasts_values_to_clientLoops() { + func test_share_multicasts_values_to_clientLoops() async { let tasksHaveFinishedExpectation = expectation(description: "the tasks have finished") tasksHaveFinishedExpectation.expectedFulfillmentCount = 2 @@ -81,6 +81,6 @@ final class AsyncSequence_ShareTests: XCTestCase { tasksHaveFinishedExpectation.fulfill() } - waitForExpectations(timeout: 5) + await fulfillment(of: [tasksHaveFinishedExpectation], timeout: 5) } } diff --git a/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift b/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift index bda619d..a6b87b6 100644 --- a/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift +++ b/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift @@ -5,6 +5,7 @@ // Created by Thibault Wittemberg on 04/01/2022. // +import AsyncAlgorithms @testable import AsyncExtensions import XCTest @@ -40,15 +41,15 @@ private struct LongAsyncSequence: AsyncSequence, AsyncIteratorProtocol } mutating func next() async throws -> Element? { - return try await withTaskCancellationHandler { [onCancel] in - onCancel() - } operation: { + return try await withTaskCancellationHandler { try await Task.sleep(nanoseconds: self.interval.nanoseconds) self.currentIndex += 1 if self.currentIndex == self.failAt { throw MockError(code: 0) } return self.elements.next() + } onCancel: { [onCancel] in + onCancel() } } @@ -102,10 +103,10 @@ final class AsyncSwitchToLatestSequenceTests: XCTestCase { func testSwitchToLatest_propagates_errors_when_base_sequence_fails() async { let sequences = [ - AsyncLazySequence([1, 2, 3]).eraseToAnyAsyncSequence(), - AsyncLazySequence([4, 5, 6]).eraseToAnyAsyncSequence(), - AsyncLazySequence([7, 8, 9]).eraseToAnyAsyncSequence(), // should fail here - AsyncLazySequence([10, 11, 12]).eraseToAnyAsyncSequence(), + [1, 2, 3].async.eraseToAnyAsyncSequence(), + [4, 5, 6].async.eraseToAnyAsyncSequence(), + [7, 8, 9].async.eraseToAnyAsyncSequence(), // should fail here + [10, 11, 12].async.eraseToAnyAsyncSequence(), ] let sourceSequence = LongAsyncSequence(elements: sequences, interval: .milliseconds(100), failAt: 2) @@ -151,7 +152,7 @@ final class AsyncSwitchToLatestSequenceTests: XCTestCase { } } - func testSwitchToLatest_finishes_when_task_is_cancelled_after_switched() { + func testSwitchToLatest_finishes_when_task_is_cancelled_after_switched() async { let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExceptation = expectation(description: "The task has been cancelled") let taskHasFinishedExpectation = expectation(description: "The task has finished") @@ -169,18 +170,18 @@ final class AsyncSwitchToLatestSequenceTests: XCTestCase { for try await element in sut { firstElement = element canCancelExpectation.fulfill() - wait(for: [hasCancelExceptation], timeout: 5) + await fulfillment(of: [hasCancelExceptation], timeout: 5) } XCTAssertEqual(firstElement, 3) taskHasFinishedExpectation.fulfill() } - wait(for: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task + await fulfillment(of: [canCancelExpectation], timeout: 5) // one element has been emitted, we can cancel the task task.cancel() hasCancelExceptation.fulfill() // we can release the lock in the for loop - wait(for: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished + await fulfillment(of: [taskHasFinishedExpectation], timeout: 5) // task has been cancelled and has finished } }