From 85e6ad2629f0e7d13dd4100d79eb1daf61014cae Mon Sep 17 00:00:00 2001 From: harry lachenmayer Date: Wed, 6 Dec 2023 17:36:33 +0000 Subject: [PATCH 01/23] fix: remove overlapping operators from swift-async-algorithms --- Package.resolved | 13 +- Package.swift | 10 +- README.md | 7 - .../Combiners/Merge/AsyncMerge2Sequence.swift | 63 -- .../Combiners/Merge/AsyncMerge3Sequence.swift | 69 --- .../Combiners/Merge/AsyncMergeSequence.swift | 57 -- .../Combiners/Merge/MergeStateMachine.swift | 249 -------- Sources/Combiners/Zip/AsyncZip2Sequence.swift | 61 -- Sources/Combiners/Zip/AsyncZip3Sequence.swift | 66 --- Sources/Combiners/Zip/AsyncZipSequence.swift | 56 -- Sources/Combiners/Zip/Zip2Runtime.swift | 214 ------- Sources/Combiners/Zip/Zip2StateMachine.swift | 373 ------------ Sources/Combiners/Zip/Zip3Runtime.swift | 252 -------- Sources/Combiners/Zip/Zip3StateMachine.swift | 542 ------------------ Sources/Combiners/Zip/ZipRuntime.swift | 186 ------ Sources/Combiners/Zip/ZipStateMachine.swift | 335 ----------- Sources/Creators/AsyncLazySequence.swift | 51 -- .../Merge/AsyncMergeSequenceTests.swift | 292 ---------- .../Combiners/Zip/AsyncZipSequenceTests.swift | 415 -------------- Tests/Creators/AsyncLazySequenceTests.swift | 50 -- .../Operators/AsyncPrependSequenceTests.swift | 1 + .../Operators/AsyncSequence+AssignTests.swift | 3 +- .../AsyncSequence+FlatMapLatestTests.swift | 5 +- .../AsyncSwitchToLatestSequenceTests.swift | 9 +- 24 files changed, 30 insertions(+), 3349 deletions(-) delete mode 100644 Sources/Combiners/Merge/AsyncMerge2Sequence.swift delete mode 100644 Sources/Combiners/Merge/AsyncMerge3Sequence.swift delete mode 100644 Sources/Combiners/Merge/AsyncMergeSequence.swift delete mode 100644 Sources/Combiners/Merge/MergeStateMachine.swift delete mode 100644 Sources/Combiners/Zip/AsyncZip2Sequence.swift delete mode 100644 Sources/Combiners/Zip/AsyncZip3Sequence.swift delete mode 100644 Sources/Combiners/Zip/AsyncZipSequence.swift delete mode 100644 Sources/Combiners/Zip/Zip2Runtime.swift delete mode 100644 Sources/Combiners/Zip/Zip2StateMachine.swift delete mode 100644 Sources/Combiners/Zip/Zip3Runtime.swift delete mode 100644 Sources/Combiners/Zip/Zip3StateMachine.swift delete mode 100644 Sources/Combiners/Zip/ZipRuntime.swift delete mode 100644 Sources/Combiners/Zip/ZipStateMachine.swift delete mode 100644 Sources/Creators/AsyncLazySequence.swift delete mode 100644 Tests/Combiners/Merge/AsyncMergeSequenceTests.swift delete mode 100644 Tests/Combiners/Zip/AsyncZipSequenceTests.swift delete mode 100644 Tests/Creators/AsyncLazySequenceTests.swift diff --git a/Package.resolved b/Package.resolved index e378654..1241cb6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,22 @@ { "object": { "pins": [ + { + "package": "swift-async-algorithms", + "repositoryURL": "https://github.com/apple/swift-async-algorithms.git", + "state": { + "branch": null, + "revision": "da4e36f86544cdf733a40d59b3a2267e3a7bbf36", + "version": "1.0.0" + } + }, { "package": "swift-collections", "repositoryURL": "https://github.com/apple/swift-collections.git", "state": { "branch": null, - "revision": "f504716c27d2e5d4144fa4794b12129301d17729", - "version": "1.0.3" + "revision": "a902f1823a7ff3c9ab2fba0f992396b948eda307", + "version": "1.0.5" } } ] diff --git a/Package.swift b/Package.swift index 4f5d569..cb582c4 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,10 @@ 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-async-algorithms.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.3")) + ], targets: [ .target( name: "AsyncExtensions", @@ -32,7 +35,10 @@ let package = Package( ), .testTarget( name: "AsyncExtensionsTests", - dependencies: ["AsyncExtensions"], + dependencies: [ + "AsyncExtensions", + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms") + ], path: "Tests"), ] ) diff --git a/README.md b/README.md index 30fe0ef..f04b3c6 100644 --- a/README.md +++ b/README.md @@ -44,12 +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 +52,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/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/AsyncMergeSequence.swift b/Sources/Combiners/Merge/AsyncMergeSequence.swift deleted file mode 100644 index ad85bf1..0000000 --- a/Sources/Combiners/Merge/AsyncMergeSequence.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// AsyncMergeSequence.swift -// -// -// Created by Thibault Wittemberg on 31/03/2022. -// - -/// Creates an asynchronous sequence of elements from many underlying asynchronous sequences -public func merge( - _ bases: Base... -) -> AsyncMergeSequence { - AsyncMergeSequence(bases) -} - -/// An asynchronous sequence of elements from many underlying asynchronous sequences -/// -/// In a `AsyncMergeSequence` 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 `AsyncMergeSequence`. -public struct AsyncMergeSequence: AsyncSequence { - public typealias Element = Base.Element - public typealias AsyncIterator = Iterator - - let bases: [Base] - - public init(_ bases: [Base]) { - self.bases = bases - } - - public func makeAsyncIterator() -> Iterator { - Iterator( - bases: self.bases - ) - } - - public struct Iterator: AsyncIteratorProtocol { - let mergeStateMachine: MergeStateMachine - - init(bases: [Base]) { - self.mergeStateMachine = MergeStateMachine( - bases - ) - } - - 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 AsyncMergeSequence: Sendable where Base: Sendable {} diff --git a/Sources/Combiners/Merge/MergeStateMachine.swift b/Sources/Combiners/Merge/MergeStateMachine.swift deleted file mode 100644 index 3cbec30..0000000 --- a/Sources/Combiners/Merge/MergeStateMachine.swift +++ /dev/null @@ -1,249 +0,0 @@ -// -// MergeStateMachine.swift -// -// -// Created by Thibault Wittemberg on 08/09/2022. -// - -import DequeModule - -struct MergeStateMachine: Sendable { - enum BufferState { - case idle - case queued(Deque>) - case awaiting(UnsafeContinuation, Never>) - case closed - } - - struct State { - var buffer: BufferState - var basesToTerminate: Int - } - - struct OnNextDecision { - let continuation: UnsafeContinuation, Never> - let regulatedElement: RegulatedElement - } - - let requestNextRegulatedElements: @Sendable () -> Void - let state: ManagedCriticalState - let task: Task - - init( - _ base1: Base1, - _ base2: Base2 - ) where Base1.Element == Element, Base2.Element == Element { - self.state = ManagedCriticalState(State(buffer: .idle, basesToTerminate: 2)) - - let regulator1 = Regulator(base1, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) }) - let regulator2 = Regulator(base2, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) }) - - self.requestNextRegulatedElements = { - regulator1.requestNextRegulatedElement() - regulator2.requestNextRegulatedElement() - } - - self.task = Task { - await withTaskGroup(of: Void.self) { group in - group.addTask { - await regulator1.iterate() - } - - group.addTask { - await regulator2.iterate() - } - } - } - } - - init( - _ base1: Base1, - _ base2: Base2, - _ base3: Base3 - ) where Base1.Element == Element, Base2.Element == Element, Base3.Element == Base1.Element { - self.state = ManagedCriticalState(State(buffer: .idle, basesToTerminate: 3)) - - let regulator1 = Regulator(base1, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) }) - let regulator2 = Regulator(base2, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) }) - let regulator3 = Regulator(base3, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) }) - - self.requestNextRegulatedElements = { - regulator1.requestNextRegulatedElement() - regulator2.requestNextRegulatedElement() - regulator3.requestNextRegulatedElement() - } - - self.task = Task { - await withTaskGroup(of: Void.self) { group in - group.addTask { - await regulator1.iterate() - } - - group.addTask { - await regulator2.iterate() - } - - group.addTask { - await regulator3.iterate() - } - } - } - } - - init( - _ bases: [Base] - ) where Base.Element == Element { - self.state = ManagedCriticalState(State(buffer: .idle, basesToTerminate: bases.count)) - - var regulators = [Regulator]() - - for base in bases { - let regulator = Regulator(base, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) }) - regulators.append(regulator) - } - - let immutableRegulators = regulators - self.requestNextRegulatedElements = { - for regulator in immutableRegulators { - regulator.requestNextRegulatedElement() - } - } - - self.task = Task { - await withTaskGroup(of: Void.self) { group in - for regulators in immutableRegulators { - group.addTask { - await regulators.iterate() - } - } - } - } - } - - @Sendable - static func onNextRegulatedElement(_ element: RegulatedElement, state: ManagedCriticalState) { - let decision = state.withCriticalRegion { state -> OnNextDecision? in - switch (state.buffer, element) { - case (.idle, .element): - state.buffer = .queued([element]) - return nil - case (.queued(var elements), .element): - elements.append(element) - state.buffer = .queued(elements) - return nil - case (.awaiting(let continuation), .element(.success)): - state.buffer = .idle - return OnNextDecision(continuation: continuation, regulatedElement: element) - case (.awaiting(let continuation), .element(.failure)): - state.buffer = .closed - return OnNextDecision(continuation: continuation, regulatedElement: element) - - case (.idle, .termination): - state.basesToTerminate -= 1 - if state.basesToTerminate == 0 { - state.buffer = .closed - } else { - state.buffer = .idle - } - return nil - - case (.queued(var elements), .termination): - state.basesToTerminate -= 1 - if state.basesToTerminate == 0 { - elements.append(.termination) - state.buffer = .queued(elements) - } - return nil - - case (.awaiting(let continuation), .termination): - state.basesToTerminate -= 1 - if state.basesToTerminate == 0 { - state.buffer = .closed - return OnNextDecision(continuation: continuation, regulatedElement: .termination) - } else { - state.buffer = .awaiting(continuation) - return nil - } - - case (.closed, _): - return nil - } - } - - if let decision = decision { - decision.continuation.resume(returning: decision.regulatedElement) - } - } - - @Sendable - func unsuspendAndClearOnCancel() { - let continuation = self.state.withCriticalRegion { state -> UnsafeContinuation, Never>? in - switch state.buffer { - case .awaiting(let continuation): - state.basesToTerminate = 0 - state.buffer = .closed - return continuation - default: - state.basesToTerminate = 0 - state.buffer = .closed - return nil - } - } - - continuation?.resume(returning: .termination) - self.task.cancel() - } - - func next() async -> RegulatedElement { - await withTaskCancellationHandler { - self.unsuspendAndClearOnCancel() - } operation: { - self.requestNextRegulatedElements() - - let regulatedElement = await withUnsafeContinuation { (continuation: UnsafeContinuation, Never>) in - let decision = self.state.withCriticalRegion { state -> OnNextDecision? in - switch state.buffer { - case .queued(var elements): - guard let regulatedElement = elements.popFirst() else { - assertionFailure("The buffer cannot by empty, it should be idle in this case") - return OnNextDecision(continuation: continuation, regulatedElement: .termination) - } - switch regulatedElement { - case .termination: - state.buffer = .closed - return OnNextDecision(continuation: continuation, regulatedElement: .termination) - case .element(.success): - if elements.isEmpty { - state.buffer = .idle - } else { - state.buffer = .queued(elements) - } - return OnNextDecision(continuation: continuation, regulatedElement: regulatedElement) - case .element(.failure): - state.buffer = .closed - return OnNextDecision(continuation: continuation, regulatedElement: regulatedElement) - } - case .idle: - state.buffer = .awaiting(continuation) - return nil - case .awaiting: - assertionFailure("The next function cannot be called concurrently") - return OnNextDecision(continuation: continuation, regulatedElement: .termination) - case .closed: - return OnNextDecision(continuation: continuation, regulatedElement: .termination) - } - } - - if let decision = decision { - decision.continuation.resume(returning: decision.regulatedElement) - } - } - - if case .termination = regulatedElement, case .element(.failure) = regulatedElement { - self.task.cancel() - } - - return regulatedElement - } - } -} 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/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/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/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/Operators/AsyncPrependSequenceTests.swift b/Tests/Operators/AsyncPrependSequenceTests.swift index e520e06..adfd84c 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 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..f8dccb2 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() diff --git a/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift b/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift index bda619d..1be42eb 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 @@ -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) From c9e1a62c5b7ca44b562116d9fd843d548b741792 Mon Sep 17 00:00:00 2001 From: harry lachenmayer Date: Wed, 6 Dec 2023 17:45:02 +0000 Subject: [PATCH 02/23] docs: remove sentence about overlaps in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f04b3c6..52a13b7 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 From ce1bf5dfeef6cd767e4e5ca343df5501cb461b2d Mon Sep 17 00:00:00 2001 From: harry lachenmayer Date: Wed, 6 Dec 2023 17:51:36 +0000 Subject: [PATCH 03/23] re-add variadic merge --- README.md | 1 + .../Combiners/Merge/AsyncMergeSequence.swift | 57 ++++ .../Combiners/Merge/MergeStateMachine.swift | 249 ++++++++++++++++++ 3 files changed, 307 insertions(+) create mode 100644 Sources/Combiners/Merge/AsyncMergeSequence.swift create mode 100644 Sources/Combiners/Merge/MergeStateMachine.swift diff --git a/README.md b/README.md index 52a13b7..71a93d0 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ AsyncStream) * [AsyncThrowingReplaySubject](./Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift): Throwing subject with a shared output. Maintain an replays a buffered amount of values ### Combiners +* [`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 diff --git a/Sources/Combiners/Merge/AsyncMergeSequence.swift b/Sources/Combiners/Merge/AsyncMergeSequence.swift new file mode 100644 index 0000000..ad85bf1 --- /dev/null +++ b/Sources/Combiners/Merge/AsyncMergeSequence.swift @@ -0,0 +1,57 @@ +// +// AsyncMergeSequence.swift +// +// +// Created by Thibault Wittemberg on 31/03/2022. +// + +/// Creates an asynchronous sequence of elements from many underlying asynchronous sequences +public func merge( + _ bases: Base... +) -> AsyncMergeSequence { + AsyncMergeSequence(bases) +} + +/// An asynchronous sequence of elements from many underlying asynchronous sequences +/// +/// In a `AsyncMergeSequence` 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 `AsyncMergeSequence`. +public struct AsyncMergeSequence: AsyncSequence { + public typealias Element = Base.Element + public typealias AsyncIterator = Iterator + + let bases: [Base] + + public init(_ bases: [Base]) { + self.bases = bases + } + + public func makeAsyncIterator() -> Iterator { + Iterator( + bases: self.bases + ) + } + + public struct Iterator: AsyncIteratorProtocol { + let mergeStateMachine: MergeStateMachine + + init(bases: [Base]) { + self.mergeStateMachine = MergeStateMachine( + bases + ) + } + + 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 AsyncMergeSequence: Sendable where Base: Sendable {} diff --git a/Sources/Combiners/Merge/MergeStateMachine.swift b/Sources/Combiners/Merge/MergeStateMachine.swift new file mode 100644 index 0000000..3cbec30 --- /dev/null +++ b/Sources/Combiners/Merge/MergeStateMachine.swift @@ -0,0 +1,249 @@ +// +// MergeStateMachine.swift +// +// +// Created by Thibault Wittemberg on 08/09/2022. +// + +import DequeModule + +struct MergeStateMachine: Sendable { + enum BufferState { + case idle + case queued(Deque>) + case awaiting(UnsafeContinuation, Never>) + case closed + } + + struct State { + var buffer: BufferState + var basesToTerminate: Int + } + + struct OnNextDecision { + let continuation: UnsafeContinuation, Never> + let regulatedElement: RegulatedElement + } + + let requestNextRegulatedElements: @Sendable () -> Void + let state: ManagedCriticalState + let task: Task + + init( + _ base1: Base1, + _ base2: Base2 + ) where Base1.Element == Element, Base2.Element == Element { + self.state = ManagedCriticalState(State(buffer: .idle, basesToTerminate: 2)) + + let regulator1 = Regulator(base1, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) }) + let regulator2 = Regulator(base2, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) }) + + self.requestNextRegulatedElements = { + regulator1.requestNextRegulatedElement() + regulator2.requestNextRegulatedElement() + } + + self.task = Task { + await withTaskGroup(of: Void.self) { group in + group.addTask { + await regulator1.iterate() + } + + group.addTask { + await regulator2.iterate() + } + } + } + } + + init( + _ base1: Base1, + _ base2: Base2, + _ base3: Base3 + ) where Base1.Element == Element, Base2.Element == Element, Base3.Element == Base1.Element { + self.state = ManagedCriticalState(State(buffer: .idle, basesToTerminate: 3)) + + let regulator1 = Regulator(base1, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) }) + let regulator2 = Regulator(base2, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) }) + let regulator3 = Regulator(base3, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) }) + + self.requestNextRegulatedElements = { + regulator1.requestNextRegulatedElement() + regulator2.requestNextRegulatedElement() + regulator3.requestNextRegulatedElement() + } + + self.task = Task { + await withTaskGroup(of: Void.self) { group in + group.addTask { + await regulator1.iterate() + } + + group.addTask { + await regulator2.iterate() + } + + group.addTask { + await regulator3.iterate() + } + } + } + } + + init( + _ bases: [Base] + ) where Base.Element == Element { + self.state = ManagedCriticalState(State(buffer: .idle, basesToTerminate: bases.count)) + + var regulators = [Regulator]() + + for base in bases { + let regulator = Regulator(base, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) }) + regulators.append(regulator) + } + + let immutableRegulators = regulators + self.requestNextRegulatedElements = { + for regulator in immutableRegulators { + regulator.requestNextRegulatedElement() + } + } + + self.task = Task { + await withTaskGroup(of: Void.self) { group in + for regulators in immutableRegulators { + group.addTask { + await regulators.iterate() + } + } + } + } + } + + @Sendable + static func onNextRegulatedElement(_ element: RegulatedElement, state: ManagedCriticalState) { + let decision = state.withCriticalRegion { state -> OnNextDecision? in + switch (state.buffer, element) { + case (.idle, .element): + state.buffer = .queued([element]) + return nil + case (.queued(var elements), .element): + elements.append(element) + state.buffer = .queued(elements) + return nil + case (.awaiting(let continuation), .element(.success)): + state.buffer = .idle + return OnNextDecision(continuation: continuation, regulatedElement: element) + case (.awaiting(let continuation), .element(.failure)): + state.buffer = .closed + return OnNextDecision(continuation: continuation, regulatedElement: element) + + case (.idle, .termination): + state.basesToTerminate -= 1 + if state.basesToTerminate == 0 { + state.buffer = .closed + } else { + state.buffer = .idle + } + return nil + + case (.queued(var elements), .termination): + state.basesToTerminate -= 1 + if state.basesToTerminate == 0 { + elements.append(.termination) + state.buffer = .queued(elements) + } + return nil + + case (.awaiting(let continuation), .termination): + state.basesToTerminate -= 1 + if state.basesToTerminate == 0 { + state.buffer = .closed + return OnNextDecision(continuation: continuation, regulatedElement: .termination) + } else { + state.buffer = .awaiting(continuation) + return nil + } + + case (.closed, _): + return nil + } + } + + if let decision = decision { + decision.continuation.resume(returning: decision.regulatedElement) + } + } + + @Sendable + func unsuspendAndClearOnCancel() { + let continuation = self.state.withCriticalRegion { state -> UnsafeContinuation, Never>? in + switch state.buffer { + case .awaiting(let continuation): + state.basesToTerminate = 0 + state.buffer = .closed + return continuation + default: + state.basesToTerminate = 0 + state.buffer = .closed + return nil + } + } + + continuation?.resume(returning: .termination) + self.task.cancel() + } + + func next() async -> RegulatedElement { + await withTaskCancellationHandler { + self.unsuspendAndClearOnCancel() + } operation: { + self.requestNextRegulatedElements() + + let regulatedElement = await withUnsafeContinuation { (continuation: UnsafeContinuation, Never>) in + let decision = self.state.withCriticalRegion { state -> OnNextDecision? in + switch state.buffer { + case .queued(var elements): + guard let regulatedElement = elements.popFirst() else { + assertionFailure("The buffer cannot by empty, it should be idle in this case") + return OnNextDecision(continuation: continuation, regulatedElement: .termination) + } + switch regulatedElement { + case .termination: + state.buffer = .closed + return OnNextDecision(continuation: continuation, regulatedElement: .termination) + case .element(.success): + if elements.isEmpty { + state.buffer = .idle + } else { + state.buffer = .queued(elements) + } + return OnNextDecision(continuation: continuation, regulatedElement: regulatedElement) + case .element(.failure): + state.buffer = .closed + return OnNextDecision(continuation: continuation, regulatedElement: regulatedElement) + } + case .idle: + state.buffer = .awaiting(continuation) + return nil + case .awaiting: + assertionFailure("The next function cannot be called concurrently") + return OnNextDecision(continuation: continuation, regulatedElement: .termination) + case .closed: + return OnNextDecision(continuation: continuation, regulatedElement: .termination) + } + } + + if let decision = decision { + decision.continuation.resume(returning: decision.regulatedElement) + } + } + + if case .termination = regulatedElement, case .element(.failure) = regulatedElement { + self.task.cancel() + } + + return regulatedElement + } + } +} From 463cd0d3073e73222d012ae00d9bdb89fba086e2 Mon Sep 17 00:00:00 2001 From: harry lachenmayer Date: Mon, 18 Dec 2023 16:33:22 +0000 Subject: [PATCH 04/23] Revert "Merge pull request #32 from sideeffect-io/fix/multicast-upstream-cancellation" This reverts commit 1286bef39dd1d6601eb4a872c5f2cc2071963bd7, reversing changes made to f5187fab0400e07e394f9d29d3acb0d4ebf3b467. --- .../AsyncCurrentValueSubject.swift | 26 ++++---- .../AsyncPassthroughSubject.swift | 26 ++++---- .../AsyncSubjects/AsyncReplaySubject.swift | 26 ++++---- .../AsyncThrowingCurrentValueSubject.swift | 34 +++++----- .../AsyncThrowingPassthroughSubject.swift | 33 +++++----- .../AsyncThrowingReplaySubject.swift | 34 +++++----- .../Operators/AsyncMulticastSequence.swift | 53 +++++++--------- .../AsyncMulticastSequenceTests.swift | 62 +++++++++++++++++++ .../Operators/AsyncSequence+ShareTests.swift | 6 +- 9 files changed, 188 insertions(+), 112 deletions(-) diff --git a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift index 5225105..6dfed67 100644 --- a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift @@ -67,24 +67,28 @@ 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() } } @@ -134,10 +138,10 @@ public final class AsyncCurrentValueSubject: AsyncSubject where Element } public mutating func next() async -> Element? { - await withTaskCancellationHandler { - await self.iterator.next() - } onCancel: { [unregister] in + await withTaskCancellationHandler { [unregister] in unregister() + } operation: { + await self.iterator.next() } } } diff --git a/Sources/AsyncSubjects/AsyncPassthroughSubject.swift b/Sources/AsyncSubjects/AsyncPassthroughSubject.swift index 61690f1..bb66f86 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() } } @@ -116,10 +120,10 @@ public final class AsyncPassthroughSubject: AsyncSubject { } public mutating func next() async -> Element? { - await withTaskCancellationHandler { - await self.iterator.next() - } onCancel: { [unregister] in + await withTaskCancellationHandler { [unregister] in unregister() + } operation: { + await self.iterator.next() } } } diff --git a/Sources/AsyncSubjects/AsyncReplaySubject.swift b/Sources/AsyncSubjects/AsyncReplaySubject.swift index f4e610e..2b7a9bc 100644 --- a/Sources/AsyncSubjects/AsyncReplaySubject.swift +++ b/Sources/AsyncSubjects/AsyncReplaySubject.swift @@ -46,29 +46,33 @@ 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() } } @@ -120,10 +124,10 @@ public final class AsyncReplaySubject: AsyncSubject where Element: Send } public mutating func next() async -> Element? { - await withTaskCancellationHandler { - await self.iterator.next() - } onCancel: { [unregister] in + await withTaskCancellationHandler { [unregister] in unregister() + } operation: { + await self.iterator.next() } } } diff --git a/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift index 2294b09..494123e 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) } } } @@ -145,10 +149,10 @@ public final class AsyncThrowingCurrentValueSubject: As } public mutating func next() async throws -> Element? { - try await withTaskCancellationHandler { - try await self.iterator.next() - } onCancel: { [unregister] in + try await withTaskCancellationHandler { [unregister] in unregister() + } operation: { + try await self.iterator.next() } } } diff --git a/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift b/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift index c1da4a5..c2a1eb4 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) } } } @@ -129,10 +132,10 @@ public final class AsyncThrowingPassthroughSubject: Asy } public mutating func next() async throws -> Element? { - try await withTaskCancellationHandler { - try await self.iterator.next() - } onCancel: { [unregister] in + try await withTaskCancellationHandler { [unregister] in unregister() + } operation: { + try await self.iterator.next() } } } diff --git a/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift b/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift index c736d49..709249e 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) } } } @@ -130,10 +134,10 @@ public final class AsyncThrowingReplaySubject: AsyncSub } public mutating func next() async throws -> Element? { - try await withTaskCancellationHandler { - try await self.iterator.next() - } onCancel: { [unregister] in + try await withTaskCancellationHandler { [unregister] in unregister() + } operation: { + try await self.iterator.next() } } } 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/Tests/Operators/AsyncMulticastSequenceTests.swift b/Tests/Operators/AsyncMulticastSequenceTests.swift index a77e4cf..b048967 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 @@ -156,4 +177,45 @@ final class AsyncMulticastSequenceTests: XCTestCase { XCTAssertEqual(error as? MockError, expectedError) } } + + func test_multicast_finishes_when_task_is_cancelled() { + let taskHasFinishedExpectation = expectation(description: "Task has finished") + + let stream = AsyncThrowingPassthroughSubject() + let sut = AsyncLazySequence<[Int]>([1, 2, 3, 4, 5]) + .multicast(stream) + .autoconnect() + + Task { + for try await _ in sut {} + taskHasFinishedExpectation.fulfill() + }.cancel() + + wait(for: [taskHasFinishedExpectation], timeout: 1) + } + + func test_multicast_finishes_when_task_is_cancelled_while_waiting_for_next() { + 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() + } + + wait(for: [canCancelExpectation], timeout: 1) + + task.cancel() + + wait(for: [taskHasFinishedExpectation], timeout: 1) + } } diff --git a/Tests/Operators/AsyncSequence+ShareTests.swift b/Tests/Operators/AsyncSequence+ShareTests.swift index 51b18d9..c41e336 100644 --- a/Tests/Operators/AsyncSequence+ShareTests.swift +++ b/Tests/Operators/AsyncSequence+ShareTests.swift @@ -40,15 +40,15 @@ private struct LongAsyncSequence: AsyncSequence, AsyncIteratorProtocol } mutating func next() async throws -> Element? { - return try await withTaskCancellationHandler { + return try await withTaskCancellationHandler { [onCancel] in + onCancel() + } operation: { 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() } } From fc0dc63ae356bfce10119a3c7e63663e27734698 Mon Sep 17 00:00:00 2001 From: William Taylor Date: Wed, 31 Jan 2024 22:09:10 +1100 Subject: [PATCH 05/23] Fix race condition in `AsyncCurrentValueSubject` --- Sources/AsyncSubjects/AsyncCurrentValueSubject.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift index 5225105..af221d4 100644 --- a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift @@ -91,8 +91,8 @@ public final class AsyncCurrentValueSubject: AsyncSubject where Element func handleNewConsumer() -> (iterator: AsyncBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncBufferedChannel() - 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, terminalState.isFinished { @@ -100,11 +100,10 @@ public final class AsyncCurrentValueSubject: AsyncSubject where Element 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 } From 29ad29a4adf446c8157dbafc6e9d48538c113892 Mon Sep 17 00:00:00 2001 From: William Taylor Date: Wed, 31 Jan 2024 22:39:10 +1100 Subject: [PATCH 06/23] Fix race condition in `AsyncThrowingCurrentValueSubject` --- .../AsyncSubjects/AsyncThrowingCurrentValueSubject.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift index 2294b09..1d102c0 100644 --- a/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift @@ -97,8 +97,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 +111,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 } From 369ddaf5aee3460af90d73a063e959dfcf90dedb Mon Sep 17 00:00:00 2001 From: harry lachenmayer Date: Thu, 18 Apr 2024 16:38:15 +0200 Subject: [PATCH 07/23] fix test compilation --- Tests/Operators/AsyncMulticastSequenceTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/Operators/AsyncMulticastSequenceTests.swift b/Tests/Operators/AsyncMulticastSequenceTests.swift index b048967..78c7b9d 100644 --- a/Tests/Operators/AsyncMulticastSequenceTests.swift +++ b/Tests/Operators/AsyncMulticastSequenceTests.swift @@ -182,7 +182,8 @@ final class AsyncMulticastSequenceTests: XCTestCase { let taskHasFinishedExpectation = expectation(description: "Task has finished") let stream = AsyncThrowingPassthroughSubject() - let sut = AsyncLazySequence<[Int]>([1, 2, 3, 4, 5]) + let sut = [1, 2, 3, 4, 5] + .async .multicast(stream) .autoconnect() From 83992aa80b81c6b47b793a0a6366090e73af833d Mon Sep 17 00:00:00 2001 From: harry lachenmayer Date: Tue, 23 Apr 2024 12:50:02 +0100 Subject: [PATCH 08/23] use checked continuations --- .../AsyncSwitchToLatestSequence.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/Operators/AsyncSwitchToLatestSequence.swift b/Sources/Operators/AsyncSwitchToLatestSequence.swift index 736df25..ff79c4e 100644 --- a/Sources/Operators/AsyncSwitchToLatestSequence.swift +++ b/Sources/Operators/AsyncSwitchToLatestSequence.swift @@ -46,7 +46,7 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl enum BaseState { case notStarted case idle - case waitingForChildIterator(UnsafeContinuation?, Never>) + case waitingForChildIterator(CheckedContinuation?, Never>) case newChildIteratorAvailable(Result) case processingChildIterator(Result) case finished(Result?) @@ -92,7 +92,7 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl } enum BaseDecision { - case resumeNext(UnsafeContinuation?, Never>, Task?) + case resumeNext(CheckedContinuation?, Never>, Task?) case cancelPreviousChildTask(Task?) } @@ -221,14 +221,9 @@ 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 childTask = await withCheckedContinuation { [state] (continuation: CheckedContinuation?, Never>) in let decision = state.withCriticalRegion { state -> NextDecision in switch state.base { case .newChildIteratorAvailable(let childIterator): @@ -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() + } } } } From b093edcbcb15af7ac06d8d133f7b547f0e47d43f Mon Sep 17 00:00:00 2001 From: harry lachenmayer Date: Tue, 23 Apr 2024 13:07:44 +0100 Subject: [PATCH 09/23] AsyncBufferedChannel: use checked continuations --- Sources/AsyncChannels/AsyncBufferedChannel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncChannels/AsyncBufferedChannel.swift b/Sources/AsyncChannels/AsyncBufferedChannel.swift index 8c97127..12a08b7 100644 --- a/Sources/AsyncChannels/AsyncBufferedChannel.swift +++ b/Sources/AsyncChannels/AsyncBufferedChannel.swift @@ -35,7 +35,7 @@ public final class AsyncBufferedChannel: AsyncSequence, Sendable { struct Awaiting: Hashable { let id: Int - let continuation: UnsafeContinuation? + let continuation: CheckedContinuation? static func placeHolder(id: Int) -> Awaiting { Awaiting(id: id, continuation: nil) @@ -178,7 +178,7 @@ public final class AsyncBufferedChannel: AsyncSequence, Sendable { awaiting?.continuation?.resume(returning: nil) } operation: { - await withUnsafeContinuation { [state] (continuation: UnsafeContinuation) in + await withCheckedContinuation { [state] (continuation: CheckedContinuation) in let decision = state.withCriticalRegion { state -> AwaitingDecision in let isCancelled = cancellation.withCriticalRegion { $0 } guard !isCancelled else { return .resume(nil) } From 8a43fcb5f22306927d1df1a5ec882be1b17ea178 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Tue, 18 Jul 2023 21:14:02 +0200 Subject: [PATCH 10/23] Linux build (#37) Import Locking.swift from upstream AsyncAlgorithms to enable non-Darwin builds --- Sources/Supporting/Locking.swift | 152 ++++++++++++++++++ Sources/Supporting/ManagedCriticalState.swift | 45 ------ 2 files changed, 152 insertions(+), 45 deletions(-) create mode 100644 Sources/Supporting/Locking.swift delete mode 100644 Sources/Supporting/ManagedCriticalState.swift diff --git a/Sources/Supporting/Locking.swift b/Sources/Supporting/Locking.swift new file mode 100644 index 0000000..8788546 --- /dev/null +++ b/Sources/Supporting/Locking.swift @@ -0,0 +1,152 @@ +//===----------------------------------------------------------------------===// +// +// 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) +@_implementationOnly import Darwin +#elseif canImport(Glibc) +@_implementationOnly import Glibc +#elseif canImport(WinSDK) +@_implementationOnly import WinSDK +#endif + +internal struct Lock { +#if canImport(Darwin) + typealias Primitive = os_unfair_lock +#elseif canImport(Glibc) + 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) + 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) + 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) + 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) + 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 { } From 949c7160e0f8b45b141416f737eb0fa2e3bd05a7 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Tue, 18 Jul 2023 21:13:10 +0200 Subject: [PATCH 11/23] Use OpenCombine on Linux (#37) --- Package.resolved | 9 +++++++++ Package.swift | 10 ++++++++-- Tests/AsyncSubjets/StreamedTests.swift | 4 ++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Package.resolved b/Package.resolved index e378654..2d8efbc 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "OpenCombine", + "repositoryURL": "https://github.com/OpenCombine/OpenCombine.git", + "state": { + "branch": null, + "revision": "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", + "version": "0.14.0" + } + }, { "package": "swift-collections", "repositoryURL": "https://github.com/apple/swift-collections.git", diff --git a/Package.swift b/Package.swift index 4f5d569..64bd592 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,10 @@ 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"), + ], targets: [ .target( name: "AsyncExtensions", @@ -32,7 +35,10 @@ let package = Package( ), .testTarget( name: "AsyncExtensionsTests", - dependencies: ["AsyncExtensions"], + dependencies: [ + "AsyncExtensions", + .product(name: "OpenCombine", package: "OpenCombine", condition: .when(platforms: [.linux])), + ], path: "Tests"), ] ) diff --git a/Tests/AsyncSubjets/StreamedTests.swift b/Tests/AsyncSubjets/StreamedTests.swift index 02e6951..5aa413c 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 { From 59013682448947085e68399a837923722c9696c7 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Tue, 1 Oct 2024 11:46:04 +0000 Subject: [PATCH 12/23] Android build --- Sources/Supporting/Locking.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/Supporting/Locking.swift b/Sources/Supporting/Locking.swift index 8788546..69e034d 100644 --- a/Sources/Supporting/Locking.swift +++ b/Sources/Supporting/Locking.swift @@ -15,12 +15,14 @@ @_implementationOnly import Glibc #elseif canImport(WinSDK) @_implementationOnly import WinSDK +#elseif canImport(Android) +@_implementationOnly import Android #endif internal struct Lock { #if canImport(Darwin) typealias Primitive = os_unfair_lock -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Android) typealias Primitive = pthread_mutex_t #elseif canImport(WinSDK) typealias Primitive = SRWLOCK @@ -36,7 +38,7 @@ internal struct Lock { fileprivate static func initialize(_ platformLock: PlatformLock) { #if canImport(Darwin) platformLock.initialize(to: os_unfair_lock()) -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Android) let result = pthread_mutex_init(platformLock, nil) precondition(result == 0, "pthread_mutex_init failed") #elseif canImport(WinSDK) @@ -45,7 +47,7 @@ internal struct Lock { } fileprivate static func deinitialize(_ platformLock: PlatformLock) { -#if canImport(Glibc) +#if canImport(Glibc) || canImport(Android) let result = pthread_mutex_destroy(platformLock) precondition(result == 0, "pthread_mutex_destroy failed") #endif @@ -55,7 +57,7 @@ internal struct Lock { fileprivate static func lock(_ platformLock: PlatformLock) { #if canImport(Darwin) os_unfair_lock_lock(platformLock) -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Android) pthread_mutex_lock(platformLock) #elseif canImport(WinSDK) AcquireSRWLockExclusive(platformLock) @@ -65,7 +67,7 @@ internal struct Lock { fileprivate static func unlock(_ platformLock: PlatformLock) { #if canImport(Darwin) os_unfair_lock_unlock(platformLock) -#elseif canImport(Glibc) +#elseif canImport(Glibc) || canImport(Android) let result = pthread_mutex_unlock(platformLock) precondition(result == 0, "pthread_mutex_unlock failed") #elseif canImport(WinSDK) From 7a096ed7d549bcf953500a1899aec8b8f6492d4e Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 1 Nov 2024 11:07:12 +1100 Subject: [PATCH 13/23] workaround for swiftlang/swift#77315 compiler crash --- Sources/AsyncSubjects/AsyncCurrentValueSubject.swift | 2 +- Sources/AsyncSubjects/AsyncReplaySubject.swift | 2 +- Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift | 2 +- Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift index 5225105..dd5b111 100644 --- a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift @@ -91,7 +91,7 @@ public final class AsyncCurrentValueSubject: AsyncSubject where Element func handleNewConsumer() -> (iterator: AsyncBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncBufferedChannel() - let (terminalState, current) = self.state.withCriticalRegion { state -> (Termination?, Element) in + let (terminalState, current) = self.state.withCriticalRegion { state in (state.terminalState, state.current) } diff --git a/Sources/AsyncSubjects/AsyncReplaySubject.swift b/Sources/AsyncSubjects/AsyncReplaySubject.swift index f4e610e..b0bdd17 100644 --- a/Sources/AsyncSubjects/AsyncReplaySubject.swift +++ b/Sources/AsyncSubjects/AsyncReplaySubject.swift @@ -75,7 +75,7 @@ public final class AsyncReplaySubject: AsyncSubject where Element: Send 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..e43e107 100644 --- a/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift @@ -97,7 +97,7 @@ public final class AsyncThrowingCurrentValueSubject: As ) -> (iterator: AsyncThrowingBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncThrowingBufferedChannel() - let (terminalState, current) = self.state.withCriticalRegion { state -> (Termination?, Element) in + let (terminalState, current) = self.state.withCriticalRegion { state in (state.terminalState, state.current) } diff --git a/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift b/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift index c736d49..3cd6dba 100644 --- a/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift @@ -80,7 +80,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) } From 07518fe1874940515840d6b2c1d9e8f793f8094b Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Thu, 21 Nov 2024 11:33:01 +1100 Subject: [PATCH 14/23] update for new withTaskCancellationHandler parameter order --- .../AsyncChannels/AsyncBufferedChannel.swift | 42 +++++++++---------- .../AsyncThrowingBufferedChannel.swift | 42 +++++++++---------- .../AsyncCurrentValueSubject.swift | 6 +-- .../AsyncPassthroughSubject.swift | 6 +-- .../AsyncSubjects/AsyncReplaySubject.swift | 6 +-- .../AsyncThrowingCurrentValueSubject.swift | 6 +-- .../AsyncThrowingPassthroughSubject.swift | 6 +-- .../AsyncThrowingReplaySubject.swift | 6 +-- .../Combiners/Merge/MergeStateMachine.swift | 4 +- .../AsyncWithLatestFrom2Sequence.swift | 12 +++--- .../AsyncWithLatestFromSequence.swift | 8 ++-- Sources/Creators/AsyncTimerSequence.swift | 6 +-- Sources/Supporting/Regulator.swift | 4 +- .../Operators/AsyncSequence+ShareTests.swift | 6 +-- .../AsyncSwitchToLatestSequenceTests.swift | 6 +-- 15 files changed, 83 insertions(+), 83 deletions(-) diff --git a/Sources/AsyncChannels/AsyncBufferedChannel.swift b/Sources/AsyncChannels/AsyncBufferedChannel.swift index 12a08b7..58cfdd4 100644 --- a/Sources/AsyncChannels/AsyncBufferedChannel.swift +++ b/Sources/AsyncChannels/AsyncBufferedChannel.swift @@ -157,27 +157,7 @@ public final class AsyncBufferedChannel: AsyncSequence, Sendable { let awaitingId = self.generateId() let cancellation = ManagedCriticalState(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 withCheckedContinuation { [state] (continuation: CheckedContinuation) in let decision = state.withCriticalRegion { state -> AwaitingDecision in let isCancelled = cancellation.withCriticalRegion { $0 } @@ -218,6 +198,26 @@ public final class AsyncBufferedChannel: AsyncSequence, Sendable { onSuspend?() } } + } onCancel: { [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) } } diff --git a/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift b/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift index f34c80e..073f0b7 100644 --- a/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift +++ b/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift @@ -178,27 +178,7 @@ public final class AsyncThrowingBufferedChannel: AsyncS let awaitingId = self.generateId() let cancellation = ManagedCriticalState(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 } @@ -245,6 +225,26 @@ public final class AsyncThrowingBufferedChannel: AsyncS onSuspend?() } } + } onCancel: { [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) } } diff --git a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift index 7889cfe..958a6ca 100644 --- a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift @@ -137,10 +137,10 @@ public final class AsyncCurrentValueSubject: AsyncSubject where Element } public mutating func next() async -> Element? { - await withTaskCancellationHandler { [unregister] in - unregister() - } operation: { + await withTaskCancellationHandler { await self.iterator.next() + } onCancel: { [unregister] in + unregister() } } } diff --git a/Sources/AsyncSubjects/AsyncPassthroughSubject.swift b/Sources/AsyncSubjects/AsyncPassthroughSubject.swift index bb66f86..5fdded4 100644 --- a/Sources/AsyncSubjects/AsyncPassthroughSubject.swift +++ b/Sources/AsyncSubjects/AsyncPassthroughSubject.swift @@ -120,10 +120,10 @@ public final class AsyncPassthroughSubject: AsyncSubject { } public mutating func next() async -> Element? { - await withTaskCancellationHandler { [unregister] in - unregister() - } operation: { + await withTaskCancellationHandler { await self.iterator.next() + } onCancel: { [unregister] in + unregister() } } } diff --git a/Sources/AsyncSubjects/AsyncReplaySubject.swift b/Sources/AsyncSubjects/AsyncReplaySubject.swift index 792b5af..64d424a 100644 --- a/Sources/AsyncSubjects/AsyncReplaySubject.swift +++ b/Sources/AsyncSubjects/AsyncReplaySubject.swift @@ -124,10 +124,10 @@ public final class AsyncReplaySubject: AsyncSubject where Element: Send } public mutating func next() async -> Element? { - await withTaskCancellationHandler { [unregister] in - unregister() - } operation: { + await withTaskCancellationHandler { await self.iterator.next() + } onCancel: { [unregister] in + unregister() } } } diff --git a/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift index 04a8c1d..804a34a 100644 --- a/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift @@ -148,10 +148,10 @@ public final class AsyncThrowingCurrentValueSubject: As } public mutating func next() async throws -> Element? { - try await withTaskCancellationHandler { [unregister] in - unregister() - } operation: { + try await withTaskCancellationHandler { try await self.iterator.next() + } onCancel: { [unregister] in + unregister() } } } diff --git a/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift b/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift index c2a1eb4..374c091 100644 --- a/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift @@ -132,10 +132,10 @@ public final class AsyncThrowingPassthroughSubject: Asy } public mutating func next() async throws -> Element? { - try await withTaskCancellationHandler { [unregister] in - unregister() - } operation: { + try await withTaskCancellationHandler { try await self.iterator.next() + } onCancel: { [unregister] in + unregister() } } } diff --git a/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift b/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift index 0d0a4e2..36dc5e9 100644 --- a/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift @@ -134,10 +134,10 @@ public final class AsyncThrowingReplaySubject: AsyncSub } public mutating func next() async throws -> Element? { - try await withTaskCancellationHandler { [unregister] in - unregister() - } operation: { + try await withTaskCancellationHandler { try await self.iterator.next() + } onCancel: { [unregister] in + unregister() } } } 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/Creators/AsyncTimerSequence.swift b/Sources/Creators/AsyncTimerSequence.swift index 5633d82..a0d6146 100644 --- a/Sources/Creators/AsyncTimerSequence.swift +++ b/Sources/Creators/AsyncTimerSequence.swift @@ -78,11 +78,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/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/Operators/AsyncSequence+ShareTests.swift b/Tests/Operators/AsyncSequence+ShareTests.swift index c41e336..df7ce90 100644 --- a/Tests/Operators/AsyncSequence+ShareTests.swift +++ b/Tests/Operators/AsyncSequence+ShareTests.swift @@ -40,15 +40,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() } } diff --git a/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift b/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift index 1be42eb..9db4c72 100644 --- a/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift +++ b/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift @@ -41,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() } } From 451654fcac477bc014be295ca34c818493951978 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Thu, 21 Nov 2024 14:19:05 +1100 Subject: [PATCH 15/23] don't use @_implementationOnly --- Sources/Supporting/Locking.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Supporting/Locking.swift b/Sources/Supporting/Locking.swift index 69e034d..d70a71b 100644 --- a/Sources/Supporting/Locking.swift +++ b/Sources/Supporting/Locking.swift @@ -10,13 +10,13 @@ //===----------------------------------------------------------------------===// #if canImport(Darwin) -@_implementationOnly import Darwin +import Darwin #elseif canImport(Glibc) -@_implementationOnly import Glibc +import Glibc #elseif canImport(WinSDK) -@_implementationOnly import WinSDK +import WinSDK #elseif canImport(Android) -@_implementationOnly import Android +import Android #endif internal struct Lock { From 87791e0ef0546331b87c91609a25c7a988cc1d0c Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Thu, 21 Nov 2024 14:18:39 +1100 Subject: [PATCH 16/23] use Atomics package instead of Locking where possible --- Package.resolved | 70 +++++++++++-------- Package.swift | 22 +++--- .../AsyncChannels/AsyncBufferedChannel.swift | 18 ++--- .../AsyncThrowingBufferedChannel.swift | 18 ++--- 4 files changed, 64 insertions(+), 64 deletions(-) diff --git a/Package.resolved b/Package.resolved index 5b6a7b8..8003564 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,34 +1,42 @@ { - "object": { - "pins": [ - { - "package": "OpenCombine", - "repositoryURL": "https://github.com/OpenCombine/OpenCombine.git", - "state": { - "branch": null, - "revision": "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", - "version": "0.14.0" - } - }, - { - "package": "swift-async-algorithms", - "repositoryURL": "https://github.com/apple/swift-async-algorithms.git", - "state": { - "branch": null, - "revision": "5c8bd186f48c16af0775972700626f0b74588278", - "version": "1.0.2" - } - }, - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections.git", - "state": { - "branch": null, - "revision": "671108c96644956dddcd89dd59c203dcdb36cec7", - "version": "1.1.4" - } + "originHash" : "087b502a28884d33bd19feaf2f3bb1b8757136daaee70899f7c6bf8a8a646bb5", + "pins" : [ + { + "identity" : "opencombine", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OpenCombine/OpenCombine.git", + "state" : { + "revision" : "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", + "version" : "0.14.0" } - ] - }, - "version": 1 + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "5c8bd186f48c16af0775972700626f0b74588278", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" + } + } + ], + "version" : 3 } diff --git a/Package.swift b/Package.swift index e06dc2c..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 @@ -20,19 +20,17 @@ let package = Package( .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", @@ -41,6 +39,8 @@ let package = Package( .product(name: "OpenCombine", package: "OpenCombine", condition: .when(platforms: [.linux])), .product(name: "AsyncAlgorithms", package: "swift-async-algorithms") ], - path: "Tests"), + path: "Tests", + swiftSettings: [.swiftLanguageMode(.v5)] + ), ] ) diff --git a/Sources/AsyncChannels/AsyncBufferedChannel.swift b/Sources/AsyncChannels/AsyncBufferedChannel.swift index 58cfdd4..6f29df9 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 @@ -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,12 +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 { await withCheckedContinuation { [state] (continuation: CheckedContinuation) 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 { @@ -200,9 +198,7 @@ public final class AsyncBufferedChannel: AsyncSequence, Sendable { } } onCancel: { [state] in let awaiting = state.withCriticalRegion { state -> Awaiting? in - cancellation.withCriticalRegion { cancellation in - cancellation = true - } + cancellation.store(true, ordering: .releasing) switch state { case .awaiting(var awaitings): let awaiting = awaitings.remove(.placeHolder(id: awaitingId)) diff --git a/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift b/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift index 073f0b7..e28cef8 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 @@ -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,12 +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 { 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 { @@ -227,9 +225,7 @@ public final class AsyncThrowingBufferedChannel: AsyncS } } onCancel: { [state] in let awaiting = state.withCriticalRegion { state -> Awaiting? in - cancellation.withCriticalRegion { cancellation in - cancellation = true - } + cancellation.store(true, ordering: .releasing) switch state { case .awaiting(var awaitings): let awaiting = awaitings.remove(.placeHolder(id: awaitingId)) From ef938794e9e42ef4c971cc2d338e49e7d4eab4da Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Thu, 21 Nov 2024 14:46:28 +1100 Subject: [PATCH 17/23] update to use await fulfillment(of:timeout:) testing API --- .../AsyncBufferedChannelTests.swift | 14 ++++----- .../AsyncBufferedThrowingChannelTests.swift | 18 +++++------ .../AsyncCurrentValueSubjectTests.swift | 20 ++++++------- .../AsyncPassthroughSubjectTests.swift | 24 +++++++-------- .../AsyncReplaySubjectTests.swift | 24 +++++++-------- ...syncThrowingCurrentValueSubjectTests.swift | 24 +++++++-------- ...AsyncThrowingPassthroughSubjectTests.swift | 30 +++++++++---------- .../AsyncThrowingReplaySubjectTests.swift | 28 ++++++++--------- Tests/AsyncSubjets/StreamedTests.swift | 6 ++-- .../AsyncWithLatestFrom2SequenceTests.swift | 14 ++++----- .../AsyncWithLatestFromSequenceTests.swift | 10 +++---- Tests/Creators/AsyncFailSequenceTests.swift | 6 ++-- Tests/Creators/AsyncJustSequenceTests.swift | 6 ++-- Tests/Creators/AsyncStream+PipeTests.swift | 8 ++--- .../AsyncThrowingJustSequenceTests.swift | 6 ++-- Tests/Creators/AsyncTimerSequenceTests.swift | 6 ++-- .../AsyncHandleEventsSequenceTests.swift | 18 +++++------ .../AsyncMulticastSequenceTests.swift | 22 +++++++------- .../Operators/AsyncPrependSequenceTests.swift | 8 ++--- Tests/Operators/AsyncScanSequenceTests.swift | 8 ++--- .../AsyncSequence+FlatMapLatestTests.swift | 8 ++--- .../Operators/AsyncSequence+ShareTests.swift | 4 +-- .../AsyncSwitchToLatestSequenceTests.swift | 8 ++--- 23 files changed, 160 insertions(+), 160 deletions(-) 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 5aa413c..3decaf2 100644 --- a/Tests/AsyncSubjets/StreamedTests.swift +++ b/Tests/AsyncSubjets/StreamedTests.swift @@ -24,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") @@ -44,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/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/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/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 78c7b9d..c3af0af 100644 --- a/Tests/Operators/AsyncMulticastSequenceTests.swift +++ b/Tests/Operators/AsyncMulticastSequenceTests.swift @@ -61,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 @@ -94,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 @@ -147,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) } @@ -178,7 +178,7 @@ final class AsyncMulticastSequenceTests: XCTestCase { } } - func test_multicast_finishes_when_task_is_cancelled() { + func test_multicast_finishes_when_task_is_cancelled() async { let taskHasFinishedExpectation = expectation(description: "Task has finished") let stream = AsyncThrowingPassthroughSubject() @@ -192,10 +192,10 @@ final class AsyncMulticastSequenceTests: XCTestCase { taskHasFinishedExpectation.fulfill() }.cancel() - wait(for: [taskHasFinishedExpectation], timeout: 1) + await fulfillment(of: [taskHasFinishedExpectation], timeout: 1) } - func test_multicast_finishes_when_task_is_cancelled_while_waiting_for_next() { + 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") @@ -213,10 +213,10 @@ final class AsyncMulticastSequenceTests: XCTestCase { taskHasFinishedExpectation.fulfill() } - wait(for: [canCancelExpectation], timeout: 1) + await fulfillment(of: [canCancelExpectation], timeout: 1) task.cancel() - wait(for: [taskHasFinishedExpectation], timeout: 1) + await fulfillment(of: [taskHasFinishedExpectation], timeout: 1) } } diff --git a/Tests/Operators/AsyncPrependSequenceTests.swift b/Tests/Operators/AsyncPrependSequenceTests.swift index adfd84c..6e1836d 100644 --- a/Tests/Operators/AsyncPrependSequenceTests.swift +++ b/Tests/Operators/AsyncPrependSequenceTests.swift @@ -24,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") @@ -38,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+FlatMapLatestTests.swift b/Tests/Operators/AsyncSequence+FlatMapLatestTests.swift index f8dccb2..f515aec 100644 --- a/Tests/Operators/AsyncSequence+FlatMapLatestTests.swift +++ b/Tests/Operators/AsyncSequence+FlatMapLatestTests.swift @@ -184,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") @@ -200,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 df7ce90..40d4502 100644 --- a/Tests/Operators/AsyncSequence+ShareTests.swift +++ b/Tests/Operators/AsyncSequence+ShareTests.swift @@ -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 9db4c72..a6b87b6 100644 --- a/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift +++ b/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift @@ -152,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") @@ -170,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 } } From ce02a3595bcf3a1942a1ba161305dd1c8c8f6bdf Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Thu, 21 Nov 2024 17:49:15 +1100 Subject: [PATCH 18/23] Revert "use checked continuations" This reverts commit 83992aa80b81c6b47b793a0a6366090e73af833d. --- Sources/Operators/AsyncSwitchToLatestSequence.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Operators/AsyncSwitchToLatestSequence.swift b/Sources/Operators/AsyncSwitchToLatestSequence.swift index ff79c4e..30147b8 100644 --- a/Sources/Operators/AsyncSwitchToLatestSequence.swift +++ b/Sources/Operators/AsyncSwitchToLatestSequence.swift @@ -46,7 +46,7 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl enum BaseState { case notStarted case idle - case waitingForChildIterator(CheckedContinuation?, Never>) + case waitingForChildIterator(UnsafeContinuation?, Never>) case newChildIteratorAvailable(Result) case processingChildIterator(Result) case finished(Result?) @@ -92,7 +92,7 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl } enum BaseDecision { - case resumeNext(CheckedContinuation?, Never>, Task?) + case resumeNext(UnsafeContinuation?, Never>, Task?) case cancelPreviousChildTask(Task?) } @@ -223,7 +223,7 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl return try await withTaskCancellationHandler { while true { - let childTask = await withCheckedContinuation { [state] (continuation: CheckedContinuation?, Never>) in + let childTask = await withUnsafeContinuation { [state] (continuation: UnsafeContinuation?, Never>) in let decision = state.withCriticalRegion { state -> NextDecision in switch state.base { case .newChildIteratorAvailable(let childIterator): From 20a10267cd48c09ba3b31ba4da9e83cea124d356 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Thu, 21 Nov 2024 17:50:37 +1100 Subject: [PATCH 19/23] Revert "AsyncBufferedChannel: use checked continuations" --- Sources/AsyncChannels/AsyncBufferedChannel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncChannels/AsyncBufferedChannel.swift b/Sources/AsyncChannels/AsyncBufferedChannel.swift index 6f29df9..44c7d0a 100644 --- a/Sources/AsyncChannels/AsyncBufferedChannel.swift +++ b/Sources/AsyncChannels/AsyncBufferedChannel.swift @@ -36,7 +36,7 @@ public final class AsyncBufferedChannel: AsyncSequence, Sendable { struct Awaiting: Hashable { let id: Int - let continuation: CheckedContinuation? + let continuation: UnsafeContinuation? static func placeHolder(id: Int) -> Awaiting { Awaiting(id: id, continuation: nil) @@ -156,7 +156,7 @@ public final class AsyncBufferedChannel: AsyncSequence, Sendable { let cancellation = ManagedAtomic(false) return await withTaskCancellationHandler { - await withCheckedContinuation { [state] (continuation: CheckedContinuation) in + await withUnsafeContinuation { [state] (continuation: UnsafeContinuation) in let decision = state.withCriticalRegion { state -> AwaitingDecision in let isCancelled = cancellation.load(ordering: .acquiring) guard !isCancelled else { return .resume(nil) } From 8af2021c0e6b46cb920785f64d1a293dd970ab16 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Sat, 23 Nov 2024 08:04:03 +1100 Subject: [PATCH 20/23] only take lock once in handleNewConsumer() --- .../AsyncCurrentValueSubject.swift | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift index 958a6ca..644f118 100644 --- a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift @@ -94,30 +94,30 @@ public final class AsyncCurrentValueSubject: AsyncSubject where Element func handleNewConsumer() -> (iterator: AsyncBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncBufferedChannel() - - let terminalState = self.state.withCriticalRegion { state -> Termination? in - state.terminalState - } - - if let terminalState = terminalState, terminalState.isFinished { - asyncBufferedChannel.finish() - return (asyncBufferedChannel.makeAsyncIterator(), {}) - } - - let consumerId = self.state.withCriticalRegion { state -> Int in - state.ids += 1 - state.channels[state.ids] = asyncBufferedChannel - asyncBufferedChannel.send(state.current) - return state.ids + var consumerId: Int! + var unregister: (@Sendable () -> Void)? + + 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 { From dabc2282009f0b223975f468d4e12bba31f3271f Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Sat, 23 Nov 2024 10:00:22 +1100 Subject: [PATCH 21/23] use Array instead of OrderedSet in awaitings, seems to fix memory leak --- Sources/AsyncChannels/AsyncBufferedChannel.swift | 15 ++++++++++++--- .../AsyncThrowingBufferedChannel.swift | 15 ++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Sources/AsyncChannels/AsyncBufferedChannel.swift b/Sources/AsyncChannels/AsyncBufferedChannel.swift index 44c7d0a..c25460d 100644 --- a/Sources/AsyncChannels/AsyncBufferedChannel.swift +++ b/Sources/AsyncChannels/AsyncBufferedChannel.swift @@ -70,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 { @@ -182,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: @@ -201,7 +207,10 @@ public final class AsyncBufferedChannel: AsyncSequence, Sendable { cancellation.store(true, ordering: .releasing) switch state { case .awaiting(var awaitings): - let awaiting = awaitings.remove(.placeHolder(id: awaitingId)) + 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 { diff --git a/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift b/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift index e28cef8..b3579f6 100644 --- a/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift +++ b/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift @@ -81,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 { @@ -206,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): @@ -228,7 +234,10 @@ public final class AsyncThrowingBufferedChannel: AsyncS cancellation.store(true, ordering: .releasing) switch state { case .awaiting(var awaitings): - let awaiting = awaitings.remove(.placeHolder(id: awaitingId)) + 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 { From 4913d7a6f15534e1937ec86f94253b5dc9c6e20e Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 3 Jan 2025 19:43:39 +1100 Subject: [PATCH 22/23] remove Package.resolved --- .gitignore | 1 + Package.resolved | 42 ------------------------------------------ 2 files changed, 1 insertion(+), 42 deletions(-) delete mode 100644 Package.resolved 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 8003564..0000000 --- a/Package.resolved +++ /dev/null @@ -1,42 +0,0 @@ -{ - "originHash" : "087b502a28884d33bd19feaf2f3bb1b8757136daaee70899f7c6bf8a8a646bb5", - "pins" : [ - { - "identity" : "opencombine", - "kind" : "remoteSourceControl", - "location" : "https://github.com/OpenCombine/OpenCombine.git", - "state" : { - "revision" : "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", - "version" : "0.14.0" - } - }, - { - "identity" : "swift-async-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-async-algorithms.git", - "state" : { - "revision" : "5c8bd186f48c16af0775972700626f0b74588278", - "version" : "1.0.2" - } - }, - { - "identity" : "swift-atomics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-atomics", - "state" : { - "revision" : "cd142fd2f64be2100422d658e7411e39489da985", - "version" : "1.2.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections.git", - "state" : { - "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", - "version" : "1.1.4" - } - } - ], - "version" : 3 -} From c1a1af1c58c50904b17b7dd7910636a2501ef1f0 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 3 Jan 2025 19:44:35 +1100 Subject: [PATCH 23/23] don't import Foundation if FoundationEssentials/Dispatch available --- Sources/Creators/AsyncTimerSequence.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/Creators/AsyncTimerSequence.swift b/Sources/Creators/AsyncTimerSequence.swift index a0d6146..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 {