From 4f81dfac11efb9721d5f67911a9ce71336f1bff8 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Tue, 18 Jul 2023 21:14:02 +0200 Subject: [PATCH 01/25] 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 3d685a5b737f89a059b644ecaf2d599fd37f0574 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Tue, 18 Jul 2023 21:13:10 +0200 Subject: [PATCH 02/25] 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 633491ca2d99386884c5117c96be18963b441162 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Tue, 1 Oct 2024 11:46:04 +0000 Subject: [PATCH 03/25] 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 04f2c3d8e18f5a965afa7e7b8160a1f17eb26cf1 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 1 Nov 2024 11:07:12 +1100 Subject: [PATCH 04/25] 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 9d66bde3a82960ef2723a04a525ef21c68d36352 Mon Sep 17 00:00:00 2001 From: William Taylor Date: Wed, 31 Jan 2024 22:09:10 +1100 Subject: [PATCH 05/25] 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 dd5b111..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 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 d6dbbae8525d5c5956c3e8efcb1a8249afabdba7 Mon Sep 17 00:00:00 2001 From: William Taylor Date: Wed, 31 Jan 2024 22:39:10 +1100 Subject: [PATCH 06/25] 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 e43e107..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 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 4246f119c1791a030345ad7bd598e93b3330547f Mon Sep 17 00:00:00 2001 From: harry lachenmayer Date: Wed, 6 Dec 2023 17:36:33 +0000 Subject: [PATCH 07/25] fix: remove overlapping operators from swift-async-algorithms --- Package.resolved | 13 +- Package.swift | 2 + README.md | 7 - .../Combiners/Merge/AsyncMerge2Sequence.swift | 63 -- .../Combiners/Merge/AsyncMerge3Sequence.swift | 69 --- .../Combiners/Merge/AsyncMergeSequence.swift | 61 -- .../Combiners/Merge/MergeStateMachine.swift | 250 -------- 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 | 367 ------------ .../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, 24 insertions(+), 3427 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 2d8efbc..c9b0e34 100644 --- a/Package.resolved +++ b/Package.resolved @@ -10,13 +10,22 @@ "version": "0.14.0" } }, + { + "package": "swift-async-algorithms", + "repositoryURL": "https://github.com/apple/swift-async-algorithms.git", + "state": { + "branch": null, + "revision": "6c050d5ef8e1aa6342528460db614e9770d7f804", + "version": "1.1.1" + } + }, { "package": "swift-collections", "repositoryURL": "https://github.com/apple/swift-collections.git", "state": { "branch": null, - "revision": "f504716c27d2e5d4144fa4794b12129301d17729", - "version": "1.0.3" + "revision": "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version": "1.3.0" } } ] diff --git a/Package.swift b/Package.swift index 64bd592..912d6b0 100644 --- a/Package.swift +++ b/Package.swift @@ -18,6 +18,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.3")), + .package(url: "https://github.com/apple/swift-async-algorithms.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.14.0"), ], targets: [ @@ -38,6 +39,7 @@ let package = Package( dependencies: [ "AsyncExtensions", .product(name: "OpenCombine", package: "OpenCombine", condition: .when(platforms: [.linux])), + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms") ], path: "Tests"), ] diff --git a/README.md b/README.md index c5e63a4..dc728d3 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,6 @@ AsyncStream) * [AsyncThrowingReplaySubject](./Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift): Throwing subject with a shared output. Maintains and 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 c152c53..0000000 --- a/Sources/Combiners/Merge/AsyncMergeSequence.swift +++ /dev/null @@ -1,61 +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 { - private let isEmpty: Bool - let mergeStateMachine: MergeStateMachine - - init(bases: [Base]) { - isEmpty = bases.isEmpty - self.mergeStateMachine = MergeStateMachine( - bases - ) - } - - public mutating func next() async rethrows -> Element? { - guard !self.isEmpty else { return nil } - - 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 2f7984a..0000000 --- a/Sources/Combiners/Merge/MergeStateMachine.swift +++ /dev/null @@ -1,250 +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.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 .element(.failure) = regulatedElement { - self.task.cancel() - } - - return regulatedElement - } onCancel: { - self.unsuspendAndClearOnCancel() - } - } -} 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 f59cbb6..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 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()) - } onCancel: { - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.rootTaskIsCancelled() - } - - self.handle(rootTaskIsCancelledOutput: output) - } - } - - 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 9b8cc18..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 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()) - } onCancel: { - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.rootTaskIsCancelled() - } - - self.handle(rootTaskIsCancelledOutput: output) - } - } - - 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 1fa4df8..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 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() } - } onCancel: { - let output = self.stateMachine.withCriticalRegion { stateMachine in - stateMachine.rootTaskIsCancelled() - } - - self.handle(rootTaskIsCancelledOutput: output) - } - } - - 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 995f8d4..0000000 --- a/Tests/Combiners/Merge/AsyncMergeSequenceTests.swift +++ /dev/null @@ -1,367 +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 - } -} - -private struct CancellationAwareSequence: AsyncSequence, AsyncIteratorProtocol { - typealias Element = Element - typealias AsyncIterator = CancellationAwareSequence - - let onStart: @Sendable () -> Void - let onCancel: @Sendable () -> Void - var hasStarted = false - - mutating func next() async throws -> Element? { - if !hasStarted { - hasStarted = true - onStart() - } - - do { - try await Task.sleep(nanoseconds: 5_000_000_000) - return nil - } catch { - onCancel() - return nil - } - } - - 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() - await fulfillment(of: [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) - } - - func testMerge_finishes_when_empty_array_of_base() { - let sut = AsyncMergeSequence>([]) - let hasFinishedExpectation = expectation(description: "Merge has finished") - - let task = Task { - var received = [Int]() - for try await element in sut { - received.append(element) - } - XCTAssertTrue(received.isEmpty) - hasFinishedExpectation.fulfill() - } - - wait(for: [hasFinishedExpectation], timeout: 1) - - task.cancel() - } - - func testMerge_cancels_other_bases_on_error() async { - let baseStartedExpectation = expectation(description: "The blocking base has started") - let baseCancelledExpectation = expectation(description: "The blocking base has been cancelled") - - let blockingBase = CancellationAwareSequence( - onStart: { baseStartedExpectation.fulfill() }, - onCancel: { baseCancelledExpectation.fulfill() } - ) - let failingBase = TimedAsyncSequence(intervalInMills: [0, 0], sequence: [1, 2], indexOfError: 1) - - let sut = merge(failingBase, blockingBase) - var iterator = sut.makeAsyncIterator() - - do { - _ = try await iterator.next() - } catch { - XCTFail("The first element should not fail") - } - await fulfillment(of: [baseStartedExpectation], timeout: 1) - - do { - _ = try await iterator.next() - XCTFail("The iteration should fail") - } catch { - XCTAssertEqual(error as? MockError, MockError(code: 1)) - } - - await fulfillment(of: [baseCancelledExpectation], timeout: 1) - } -} diff --git a/Tests/Combiners/Zip/AsyncZipSequenceTests.swift b/Tests/Combiners/Zip/AsyncZipSequenceTests.swift deleted file mode 100644 index 68eafaa..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() - await fulfillment(of: [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() - await fulfillment(of: [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() - await fulfillment(of: [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 0f4e941..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() - await fulfillment(of: [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 c23c03d..ec0e606 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 c84a637..744674d 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 8619cde..79bf5d1 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 484ea76515edf1e3046cac7a17b84e8fbf54b676 Mon Sep 17 00:00:00 2001 From: harry lachenmayer Date: Wed, 6 Dec 2023 17:45:02 +0000 Subject: [PATCH 08/25] 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 dc728d3..620b192 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 dd3044257eb02a90dd41de8a0d75ffbfb68e44e1 Mon Sep 17 00:00:00 2001 From: harry lachenmayer Date: Wed, 6 Dec 2023 17:51:36 +0000 Subject: [PATCH 09/25] 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 620b192..ef9c82d 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ AsyncStream) * [AsyncThrowingReplaySubject](./Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift): Throwing subject with a shared output. Maintains and 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 f41a9fb4c9558996284b86cd5b1fa83a9a649ea1 Mon Sep 17 00:00:00 2001 From: harry lachenmayer Date: Mon, 18 Dec 2023 16:33:22 +0000 Subject: [PATCH 10/25] 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 | 51 ++++++++----------- .../AsyncMulticastSequenceTests.swift | 22 +++++++- .../Operators/AsyncSequence+ShareTests.swift | 6 +-- 9 files changed, 147 insertions(+), 111 deletions(-) diff --git a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift index af221d4..7889cfe 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() } } @@ -133,10 +137,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 2badeb9..baa3e61 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 b0bdd17..792b5af 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 1d102c0..04a8c1d 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) } } } @@ -144,10 +148,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 3cd6dba..0d0a4e2 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 1cc4971..a105b1a 100644 --- a/Sources/Operators/AsyncMulticastSequence.swift +++ b/Sources/Operators/AsyncMulticastSequence.swift @@ -106,37 +106,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 { @@ -164,11 +158,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 ffe9828..aee9b3f 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,5 +177,4 @@ final class AsyncMulticastSequenceTests: XCTestCase { XCTAssertEqual(error as? MockError, expectedError) } } - } 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 e3de9605c16749b8b4bea1005386cf212cbf14c7 Mon Sep 17 00:00:00 2001 From: harry lachenmayer Date: Thu, 18 Apr 2024 16:38:15 +0200 Subject: [PATCH 11/25] fix test compilation --- .../AsyncMulticastSequenceTests.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Tests/Operators/AsyncMulticastSequenceTests.swift b/Tests/Operators/AsyncMulticastSequenceTests.swift index aee9b3f..78c7b9d 100644 --- a/Tests/Operators/AsyncMulticastSequenceTests.swift +++ b/Tests/Operators/AsyncMulticastSequenceTests.swift @@ -177,4 +177,46 @@ 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 = [1, 2, 3, 4, 5] + .async + .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) + } } From 8f7a2008ed24a67c6578003b47fd61f8a7ccbfa5 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Thu, 21 Nov 2024 11:33:01 +1100 Subject: [PATCH 12/25] update for new withTaskCancellationHandler parameter order --- Sources/AsyncSubjects/AsyncCurrentValueSubject.swift | 6 +++--- Sources/AsyncSubjects/AsyncPassthroughSubject.swift | 6 +++--- Sources/AsyncSubjects/AsyncReplaySubject.swift | 6 +++--- .../AsyncSubjects/AsyncThrowingCurrentValueSubject.swift | 6 +++--- Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift | 6 +++--- Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift | 6 +++--- Sources/Combiners/Merge/MergeStateMachine.swift | 4 ++-- .../WithLatestFrom/AsyncWithLatestFromSequence.swift | 2 +- Tests/Operators/AsyncSequence+ShareTests.swift | 6 +++--- 9 files changed, 24 insertions(+), 24 deletions(-) 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 baa3e61..810a55a 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/AsyncWithLatestFromSequence.swift b/Sources/Combiners/WithLatestFrom/AsyncWithLatestFromSequence.swift index 2eb9059..6481f85 100644 --- a/Sources/Combiners/WithLatestFrom/AsyncWithLatestFromSequence.swift +++ b/Sources/Combiners/WithLatestFrom/AsyncWithLatestFromSequence.swift @@ -158,7 +158,7 @@ where Other: Sendable, Other.Element: Sendable { } onCancel: { [otherTask] in otherTask?.cancel() } - } + } } } 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() } } From cfdfcec3d2ac939207dbd2b1d903237fb9f8a478 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Thu, 21 Nov 2024 14:19:05 +1100 Subject: [PATCH 13/25] 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 84d16b6434314bf93e679c1388fcd5540180cf94 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Thu, 21 Nov 2024 14:18:39 +1100 Subject: [PATCH 14/25] 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 c9b0e34..f001951 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": "6c050d5ef8e1aa6342528460db614e9770d7f804", - "version": "1.1.1" - } - }, - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections.git", - "state": { - "branch": null, - "revision": "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", - "version": "1.3.0" - } + "originHash" : "e8921af4fb0aaa5ba2349d86103630eaa8b92a35ca3dc762e6b0154483feb818", + "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" : "6c050d5ef8e1aa6342528460db614e9770d7f804", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" + } + } + ], + "version" : 3 } diff --git a/Package.swift b/Package.swift index 912d6b0..c73e7b8 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/apple/swift-async-algorithms.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.14.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 2f28efe..e4a79eb 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, Senda } } - 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, Senda func next(onSuspend: (() -> Void)? = nil) async -> Element? { let awaitingId = self.generateId() - let cancellation = ManagedCriticalState(false) + let cancellation = ManagedAtomic(false) return await withTaskCancellationHandler { await withUnsafeContinuation { [state] (continuation: UnsafeContinuation) in let decision = state.withCriticalRegion { state -> AwaitingDecision in - let isCancelled = cancellation.withCriticalRegion { $0 } + let isCancelled = cancellation.load(ordering: .acquiring) guard !isCancelled else { return .resume(nil) } switch state { @@ -200,9 +198,7 @@ public final class AsyncBufferedChannel: AsyncSequence, Senda } } 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 b16126f4b76051d53650fe6e9a95432d46f01781 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Thu, 21 Nov 2024 14:46:28 +1100 Subject: [PATCH 15/25] update to use await fulfillment(of:timeout:) testing API --- .../AsyncCurrentValueSubjectTests.swift | 12 +++++----- .../AsyncPassthroughSubjectTests.swift | 14 ++++++------ .../AsyncReplaySubjectTests.swift | 16 +++++++------- ...syncThrowingCurrentValueSubjectTests.swift | 12 +++++----- ...AsyncThrowingPassthroughSubjectTests.swift | 14 ++++++------ .../AsyncThrowingReplaySubjectTests.swift | 16 +++++++------- Tests/AsyncSubjets/StreamedTests.swift | 6 ++--- Tests/Creators/AsyncFailSequenceTests.swift | 4 ++-- Tests/Creators/AsyncJustSequenceTests.swift | 4 ++-- Tests/Creators/AsyncStream+PipeTests.swift | 8 +++---- .../AsyncThrowingJustSequenceTests.swift | 4 ++-- Tests/Creators/AsyncTimerSequenceTests.swift | 6 ++--- .../AsyncHandleEventsSequenceTests.swift | 12 +++++----- .../AsyncMulticastSequenceTests.swift | 22 +++++++++---------- .../Operators/AsyncPrependSequenceTests.swift | 6 ++--- Tests/Operators/AsyncScanSequenceTests.swift | 6 ++--- .../AsyncSequence+FlatMapLatestTests.swift | 6 ++--- .../Operators/AsyncSequence+ShareTests.swift | 4 ++-- .../AsyncSwitchToLatestSequenceTests.swift | 6 ++--- 19 files changed, 89 insertions(+), 89 deletions(-) diff --git a/Tests/AsyncSubjets/AsyncCurrentValueSubjectTests.swift b/Tests/AsyncSubjets/AsyncCurrentValueSubjectTests.swift index 3d2d9e2..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 { @@ -118,7 +118,7 @@ final class AsyncCurrentValueSubjectTests: XCTestCase { 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") @@ -136,13 +136,13 @@ final class AsyncCurrentValueSubjectTests: XCTestCase { 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 { diff --git a/Tests/AsyncSubjets/AsyncPassthroughSubjectTests.swift b/Tests/AsyncSubjets/AsyncPassthroughSubjectTests.swift index 728ed28..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 { @@ -106,7 +106,7 @@ final class AsyncPassthroughSubjectTests: XCTestCase { 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") @@ -128,17 +128,17 @@ final class AsyncPassthroughSubjectTests: XCTestCase { 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 { diff --git a/Tests/AsyncSubjets/AsyncReplaySubjectTests.swift b/Tests/AsyncSubjets/AsyncReplaySubjectTests.swift index 0f824fc..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 { @@ -141,7 +141,7 @@ final class AsyncReplaySubjectTests: XCTestCase { 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") @@ -161,13 +161,13 @@ final class AsyncReplaySubjectTests: XCTestCase { 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 { diff --git a/Tests/AsyncSubjets/AsyncThrowingCurrentValueSubjectTests.swift b/Tests/AsyncSubjets/AsyncThrowingCurrentValueSubjectTests.swift index c2cfba0..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 { @@ -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") @@ -188,13 +188,13 @@ final class AsyncThrowingCurrentValueSubjectTests: XCTestCase { 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 { diff --git a/Tests/AsyncSubjets/AsyncThrowingPassthroughSubjectTests.swift b/Tests/AsyncSubjets/AsyncThrowingPassthroughSubjectTests.swift index e9838dc..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 { @@ -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") @@ -191,17 +191,17 @@ final class AsyncThrowingPassthroughSubjectTests: XCTestCase { 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 { diff --git a/Tests/AsyncSubjets/AsyncThrowingReplaySubjectTests.swift b/Tests/AsyncSubjets/AsyncThrowingReplaySubjectTests.swift index 222ef5a..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 { @@ -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") @@ -215,13 +215,13 @@ final class AsyncThrowingReplaySubjectTests: XCTestCase { 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 { 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/Creators/AsyncFailSequenceTests.swift b/Tests/Creators/AsyncFailSequenceTests.swift index 4f55f53..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") @@ -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 731c634..e96d9ff 100644 --- a/Tests/Creators/AsyncJustSequenceTests.swift +++ b/Tests/Creators/AsyncJustSequenceTests.swift @@ -22,7 +22,7 @@ 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") @@ -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 d5fe8be..da71bda 100644 --- a/Tests/Creators/AsyncThrowingJustSequenceTests.swift +++ b/Tests/Creators/AsyncThrowingJustSequenceTests.swift @@ -47,7 +47,7 @@ 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") @@ -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 eedd8d4..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") @@ -57,13 +57,13 @@ final class AsyncHandleEventsSequenceTests: XCTestCase { } } - 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"]) } @@ -98,7 +98,7 @@ final class AsyncHandleEventsSequenceTests: XCTestCase { 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") @@ -118,12 +118,12 @@ final class AsyncHandleEventsSequenceTests: XCTestCase { 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 ec0e606..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") @@ -44,12 +44,12 @@ final class AsyncPrependSequenceTests: XCTestCase { 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 8eeb269..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") @@ -49,12 +49,12 @@ final class AsyncScanSequenceTests: XCTestCase { 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 744674d..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") @@ -206,13 +206,13 @@ final class AsyncSequence_FlatMapLatestTests: XCTestCase { 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 79bf5d1..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") @@ -176,12 +176,12 @@ final class AsyncSwitchToLatestSequenceTests: XCTestCase { 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 ec16e3bfeaf55315a2bb2c5ae80826b9fee6cf4a Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Sat, 23 Nov 2024 08:04:03 +1100 Subject: [PATCH 16/25] 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 d050ac132b97895075e72fd98b3a4e4827e2aba5 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Sat, 23 Nov 2024 10:00:22 +1100 Subject: [PATCH 17/25] 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 e4a79eb..5b38dd3 100644 --- a/Sources/AsyncChannels/AsyncBufferedChannel.swift +++ b/Sources/AsyncChannels/AsyncBufferedChannel.swift @@ -70,7 +70,7 @@ public final class AsyncBufferedChannel: AsyncSequence, Senda 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, Senda 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, Senda 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 206d7b66271d713f42a80dc0dc748b3538d32b1c Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 3 Jan 2025 19:43:39 +1100 Subject: [PATCH 18/25] 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 f001951..0000000 --- a/Package.resolved +++ /dev/null @@ -1,42 +0,0 @@ -{ - "originHash" : "e8921af4fb0aaa5ba2349d86103630eaa8b92a35ca3dc762e6b0154483feb818", - "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" : "6c050d5ef8e1aa6342528460db614e9770d7f804", - "version" : "1.1.1" - } - }, - { - "identity" : "swift-atomics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-atomics.git", - "state" : { - "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections.git", - "state" : { - "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", - "version" : "1.3.0" - } - } - ], - "version" : 3 -} From d51c4aa3708678f7271e5506ef024b05910c6e5a Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Fri, 3 Jan 2025 19:44:35 +1100 Subject: [PATCH 19/25] 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 { From 8f1da30ac50e10cd85bdec7b869747c6d959b8ab Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Wed, 11 Feb 2026 14:43:46 +1100 Subject: [PATCH 20/25] unregister channels after iteration complete --- .../AsyncCurrentValueSubject.swift | 14 +++++++++- .../AsyncPassthroughSubject.swift | 14 +++++++++- .../AsyncSubjects/AsyncReplaySubject.swift | 14 +++++++++- .../AsyncThrowingCurrentValueSubject.swift | 26 ++++++++++++++++--- .../AsyncThrowingPassthroughSubject.swift | 26 ++++++++++++++++--- .../AsyncThrowingReplaySubject.swift | 26 ++++++++++++++++--- 6 files changed, 108 insertions(+), 12 deletions(-) diff --git a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift index 644f118..5a14728 100644 --- a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift @@ -127,6 +127,7 @@ public final class AsyncCurrentValueSubject: AsyncSubject where Element public struct Iterator: AsyncSubjectIterator { var iterator: AsyncBufferedChannel.Iterator let unregister: @Sendable () -> Void + var isFinished = false init(asyncSubject: AsyncCurrentValueSubject) { (self.iterator, self.unregister) = asyncSubject.handleNewConsumer() @@ -137,11 +138,22 @@ public final class AsyncCurrentValueSubject: AsyncSubject where Element } public mutating func next() async -> Element? { - await withTaskCancellationHandler { + // Don't proceed if we've already finished + guard !isFinished else { return nil } + + let result = await withTaskCancellationHandler { await self.iterator.next() } onCancel: { [unregister] in unregister() } + + // If iteration completed normally (returned nil), unregister the channel + if result == nil { + isFinished = true + unregister() + } + + return result } } } diff --git a/Sources/AsyncSubjects/AsyncPassthroughSubject.swift b/Sources/AsyncSubjects/AsyncPassthroughSubject.swift index 810a55a..fc26774 100644 --- a/Sources/AsyncSubjects/AsyncPassthroughSubject.swift +++ b/Sources/AsyncSubjects/AsyncPassthroughSubject.swift @@ -110,6 +110,7 @@ public final class AsyncPassthroughSubject: AsyncSubject { public struct Iterator: AsyncSubjectIterator { var iterator: AsyncBufferedChannel.Iterator let unregister: @Sendable () -> Void + var isFinished = false init(asyncSubject: AsyncPassthroughSubject) { (self.iterator, self.unregister) = asyncSubject.handleNewConsumer() @@ -120,11 +121,22 @@ public final class AsyncPassthroughSubject: AsyncSubject { } public mutating func next() async -> Element? { - await withTaskCancellationHandler { + // Don't proceed if we've already finished + guard !isFinished else { return nil } + + let result = await withTaskCancellationHandler { await self.iterator.next() } onCancel: { [unregister] in unregister() } + + // If iteration completed normally (returned nil), unregister the channel + if result == nil { + isFinished = true + unregister() + } + + return result } } } diff --git a/Sources/AsyncSubjects/AsyncReplaySubject.swift b/Sources/AsyncSubjects/AsyncReplaySubject.swift index 64d424a..b79b48f 100644 --- a/Sources/AsyncSubjects/AsyncReplaySubject.swift +++ b/Sources/AsyncSubjects/AsyncReplaySubject.swift @@ -114,6 +114,7 @@ public final class AsyncReplaySubject: AsyncSubject where Element: Send public struct Iterator: AsyncSubjectIterator { var iterator: AsyncBufferedChannel.Iterator let unregister: @Sendable () -> Void + var isFinished = false init(asyncSubject: AsyncReplaySubject) { (self.iterator, self.unregister) = asyncSubject.handleNewConsumer() @@ -124,11 +125,22 @@ public final class AsyncReplaySubject: AsyncSubject where Element: Send } public mutating func next() async -> Element? { - await withTaskCancellationHandler { + // Don't proceed if we've already finished + guard !isFinished else { return nil } + + let result = await withTaskCancellationHandler { await self.iterator.next() } onCancel: { [unregister] in unregister() } + + // If iteration completed normally (returned nil), unregister the channel + if result == nil { + isFinished = true + unregister() + } + + return result } } } diff --git a/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift index 804a34a..04ba1d4 100644 --- a/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift @@ -138,6 +138,7 @@ public final class AsyncThrowingCurrentValueSubject: As public struct Iterator: AsyncSubjectIterator { var iterator: AsyncThrowingBufferedChannel.Iterator let unregister: @Sendable () -> Void + var isFinished = false init(asyncSubject: AsyncThrowingCurrentValueSubject) { (self.iterator, self.unregister) = asyncSubject.handleNewConsumer() @@ -148,11 +149,30 @@ public final class AsyncThrowingCurrentValueSubject: As } public mutating func next() async throws -> Element? { - try await withTaskCancellationHandler { - try await self.iterator.next() - } onCancel: { [unregister] in + // Don't proceed if we've already finished + guard !isFinished else { return nil } + + let result: Element? + do { + result = try await withTaskCancellationHandler { + try await self.iterator.next() + } onCancel: { [unregister] in + unregister() + } + } catch { + // On error, mark as finished and unregister before rethrowing + isFinished = true unregister() + throw error } + + // If iteration completed normally (returned nil), unregister the channel + if result == nil { + isFinished = true + unregister() + } + + return result } } } diff --git a/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift b/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift index 374c091..b4ee14d 100644 --- a/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingPassthroughSubject.swift @@ -122,6 +122,7 @@ public final class AsyncThrowingPassthroughSubject: Asy public struct Iterator: AsyncSubjectIterator { var iterator: AsyncThrowingBufferedChannel.Iterator let unregister: @Sendable () -> Void + var isFinished = false init(asyncSubject: AsyncThrowingPassthroughSubject) { (self.iterator, self.unregister) = asyncSubject.handleNewConsumer() @@ -132,11 +133,30 @@ public final class AsyncThrowingPassthroughSubject: Asy } public mutating func next() async throws -> Element? { - try await withTaskCancellationHandler { - try await self.iterator.next() - } onCancel: { [unregister] in + // Don't proceed if we've already finished + guard !isFinished else { return nil } + + let result: Element? + do { + result = try await withTaskCancellationHandler { + try await self.iterator.next() + } onCancel: { [unregister] in + unregister() + } + } catch { + // On error, mark as finished and unregister before rethrowing + isFinished = true unregister() + throw error } + + // If iteration completed normally (returned nil), unregister the channel + if result == nil { + isFinished = true + unregister() + } + + return result } } } diff --git a/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift b/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift index 36dc5e9..d0105d2 100644 --- a/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift @@ -124,6 +124,7 @@ public final class AsyncThrowingReplaySubject: AsyncSub public struct Iterator: AsyncSubjectIterator { var iterator: AsyncThrowingBufferedChannel.Iterator let unregister: @Sendable () -> Void + var isFinished = false init(asyncSubject: AsyncThrowingReplaySubject) { (self.iterator, self.unregister) = asyncSubject.handleNewConsumer() @@ -134,11 +135,30 @@ public final class AsyncThrowingReplaySubject: AsyncSub } public mutating func next() async throws -> Element? { - try await withTaskCancellationHandler { - try await self.iterator.next() - } onCancel: { [unregister] in + // Don't proceed if we've already finished + guard !isFinished else { return nil } + + let result: Element? + do { + result = try await withTaskCancellationHandler { + try await self.iterator.next() + } onCancel: { [unregister] in + unregister() + } + } catch { + // On error, mark as finished and unregister before rethrowing + isFinished = true unregister() + throw error } + + // If iteration completed normally (returned nil), unregister the channel + if result == nil { + isFinished = true + unregister() + } + + return result } } } From 13e7d02302abb508737c1311f510c779b54f498e Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Sun, 22 Mar 2026 16:54:18 +1100 Subject: [PATCH 21/25] Fix impossible AND condition in MergeStateMachine error/termination check The comma-separated 'if case' acted as logical AND, making it impossible for regulatedElement to match both .termination and .element(.failure) simultaneously. Use a switch with OR logic instead so the task is properly cancelled on either termination or failure. Amp-Thread-ID: https://ampcode.com/threads/T-019d1415-dee4-710f-8b2b-1f45fa5c9e69 Co-authored-by: Amp --- Sources/Combiners/Merge/MergeStateMachine.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/Combiners/Merge/MergeStateMachine.swift b/Sources/Combiners/Merge/MergeStateMachine.swift index 3affdc6..9ed3e0b 100644 --- a/Sources/Combiners/Merge/MergeStateMachine.swift +++ b/Sources/Combiners/Merge/MergeStateMachine.swift @@ -237,8 +237,11 @@ struct MergeStateMachine: Sendable { } } - if case .termination = regulatedElement, case .element(.failure) = regulatedElement { - self.task.cancel() + switch regulatedElement { + case .termination, .element(.failure): + self.task.cancel() + default: + break } return regulatedElement From 06741fecb11f7b89f5d7f5b13eaae89b4c8c1026 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Sun, 22 Mar 2026 16:54:33 +1100 Subject: [PATCH 22/25] Fix race condition in AsyncThrowingCurrentValueSubject.handleNewConsumer Unify into a single critical region so that checking terminalState and registering the channel happen atomically. Previously a send(.finished) between the two separate lock acquisitions could cause the new channel to miss termination and hang forever. Amp-Thread-ID: https://ampcode.com/threads/T-019d1415-dee4-710f-8b2b-1f45fa5c9e69 Co-authored-by: Amp --- .../AsyncThrowingCurrentValueSubject.swift | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift index 04ba1d4..028dbaa 100644 --- a/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift @@ -100,35 +100,35 @@ public final class AsyncThrowingCurrentValueSubject: As func handleNewConsumer( ) -> (iterator: AsyncThrowingBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncThrowingBufferedChannel() - - let terminalState = self.state.withCriticalRegion { state -> Termination? in - state.terminalState - } - - if let terminalState = terminalState { - switch terminalState { - case .finished: - asyncBufferedChannel.finish() - case .failure(let error): - asyncBufferedChannel.fail(error) + var consumerId: Int! + var unregister: (@Sendable () -> Void)? + + self.state.withCriticalRegion { state in + let terminalState = state.terminalState + if let terminalState { + switch terminalState { + case .finished: + asyncBufferedChannel.finish() + case .failure(let error): + asyncBufferedChannel.fail(error) + } + } else { + state.ids &+= 1 + consumerId = state.ids + state.channels[consumerId] = asyncBufferedChannel + asyncBufferedChannel.send(state.current) } - 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 } - 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 c62574d682ffb9a51b2a64e1be6e1029f52b8376 Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Sun, 22 Mar 2026 16:54:52 +1100 Subject: [PATCH 23/25] Fix race condition in AsyncReplaySubject.handleNewConsumer Unify into a single critical region so that checking terminalState and registering the channel happen atomically, matching the pattern used in AsyncCurrentValueSubject. Amp-Thread-ID: https://ampcode.com/threads/T-019d1415-dee4-710f-8b2b-1f45fa5c9e69 Co-authored-by: Amp --- .../AsyncSubjects/AsyncReplaySubject.swift | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/Sources/AsyncSubjects/AsyncReplaySubject.swift b/Sources/AsyncSubjects/AsyncReplaySubject.swift index b79b48f..623b93d 100644 --- a/Sources/AsyncSubjects/AsyncReplaySubject.swift +++ b/Sources/AsyncSubjects/AsyncReplaySubject.swift @@ -78,33 +78,32 @@ 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 in - (state.terminalState, state.buffer) - } - - if let terminalState = terminalState, terminalState.isFinished { - asyncBufferedChannel.finish() - return (asyncBufferedChannel.makeAsyncIterator(), {}) - } - - for element in elements { - asyncBufferedChannel.send(element) - } - - let consumerId = self.state.withCriticalRegion { state -> Int in - state.ids += 1 - state.channels[state.ids] = asyncBufferedChannel - 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 { + for element in state.buffer { + asyncBufferedChannel.send(element) + } + state.ids &+= 1 + consumerId = state.ids + state.channels[consumerId] = asyncBufferedChannel + } } - 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 b97d381f6156e8c34b718bddfb9481f957a07edc Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Sun, 22 Mar 2026 16:55:09 +1100 Subject: [PATCH 24/25] Fix race condition in AsyncThrowingReplaySubject.handleNewConsumer Unify into a single critical region so that checking terminalState and registering the channel happen atomically, matching the pattern used in AsyncCurrentValueSubject. Amp-Thread-ID: https://ampcode.com/threads/T-019d1415-dee4-710f-8b2b-1f45fa5c9e69 Co-authored-by: Amp --- .../AsyncThrowingReplaySubject.swift | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift b/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift index d0105d2..82f9c8e 100644 --- a/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift @@ -83,38 +83,37 @@ public final class AsyncThrowingReplaySubject: AsyncSub func handleNewConsumer( ) -> (iterator: AsyncThrowingBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncThrowingBufferedChannel() - - let (terminalState, elements) = self.state.withCriticalRegion { state in - (state.terminalState, state.buffer) - } - - if let terminalState = terminalState { - switch terminalState { - case .finished: - asyncBufferedChannel.finish() - case .failure(let error): - asyncBufferedChannel.fail(error) + var consumerId: Int! + var unregister: (@Sendable () -> Void)? + + self.state.withCriticalRegion { state in + let terminalState = state.terminalState + if let terminalState { + switch terminalState { + case .finished: + asyncBufferedChannel.finish() + case .failure(let error): + asyncBufferedChannel.fail(error) + } + } else { + for element in state.buffer { + asyncBufferedChannel.send(element) + } + state.ids &+= 1 + consumerId = state.ids + state.channels[consumerId] = asyncBufferedChannel } - return (asyncBufferedChannel.makeAsyncIterator(), {}) - } - - for element in elements { - asyncBufferedChannel.send(element) } - let consumerId = self.state.withCriticalRegion { state -> Int in - state.ids += 1 - state.channels[state.ids] = asyncBufferedChannel - return state.ids - } - - 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 13a2c2b6efa298447a5f6f96f491de1d357f63ff Mon Sep 17 00:00:00 2001 From: Luke Howard Date: Thu, 21 May 2026 14:09:05 +1000 Subject: [PATCH 25/25] build: add CMake support for shared-library builds Adds a parallel CMake build alongside Package.swift that produces libAsyncExtensions.so + an installable swiftmodule, plus a find_package config (AsyncExtensionsConfig.cmake) so downstream Swift-on-Linux projects can consume AsyncExtensions as a shared library and avoid duplicate-module / type-identity issues that arise when multiple consumers in the same process pull in independent static copies. Ships FindSwiftCollections.cmake and FindSwiftAtomics.cmake shims because the upstream packages install built artifacts but no install-tree-usable Config.cmake. Note: swift-atomics' upstream CMake doesn't install the _AtomicsShims headers/module.modulemap; the prefix must have those staged manually (or upstream fixed) for the FindSwiftAtomics shim to resolve. Co-Authored-By: Claude Opus 4.7 (1M context) --- CMakeLists.txt | 32 +++++++ Sources/CMakeLists.txt | 30 ++++++ cmake/modules/AsyncExtensionsConfig.cmake.in | 21 +++++ cmake/modules/CMakeLists.txt | 38 ++++++++ cmake/modules/FindSwiftAtomics.cmake | 54 +++++++++++ cmake/modules/FindSwiftCollections.cmake | 56 +++++++++++ cmake/modules/SwiftSupport.cmake | 98 ++++++++++++++++++++ 7 files changed, 329 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 Sources/CMakeLists.txt create mode 100644 cmake/modules/AsyncExtensionsConfig.cmake.in create mode 100644 cmake/modules/CMakeLists.txt create mode 100644 cmake/modules/FindSwiftAtomics.cmake create mode 100644 cmake/modules/FindSwiftCollections.cmake create mode 100644 cmake/modules/SwiftSupport.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5ea3fbf --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,32 @@ +#[[ +This source file is part of the AsyncExtensions open source project + +Copyright (c) 2024 The AsyncExtensions project authors +Licensed under MIT License + +See https://github.com/sideeffect-io/AsyncExtensions/blob/main/LICENSE for license information +#]] + +cmake_minimum_required(VERSION 3.16) +project(AsyncExtensions + LANGUAGES Swift) + +list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/modules) + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift) + +if(CMAKE_SYSTEM_NAME STREQUAL Windows OR CMAKE_SYSTEM_NAME STREQUAL Darwin) + option(BUILD_SHARED_LIBS "Build shared libraries by default" YES) +endif() + +include(GNUInstallDirs) +include(SwiftSupport) + +find_package(SwiftCollections REQUIRED) +find_package(SwiftAtomics REQUIRED) + +add_subdirectory(Sources) +add_subdirectory(cmake/modules) diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt new file mode 100644 index 0000000..918edd4 --- /dev/null +++ b/Sources/CMakeLists.txt @@ -0,0 +1,30 @@ +#[[ +This source file is part of the AsyncExtensions open source project + +Copyright (c) 2024 The AsyncExtensions project authors +Licensed under MIT License + +See https://github.com/sideeffect-io/AsyncExtensions/blob/main/LICENSE for license information +#]] + +# Package.swift declares the AsyncExtensions target with `path: "Sources"`, so +# unlike a typical SwiftPM layout, this single target's sources span multiple +# subdirectories directly under Sources/. + +file(GLOB_RECURSE ASYNC_EXTENSIONS_SOURCES CONFIGURE_DEPENDS + *.swift) + +add_library(AsyncExtensions ${ASYNC_EXTENSIONS_SOURCES}) +target_link_libraries(AsyncExtensions + PUBLIC + SwiftCollections::Collections + SwiftAtomics::Atomics) + +get_swift_host_os(_swift_os) +set_target_properties(AsyncExtensions PROPERTIES + SOVERSION 0 + INTERFACE_INCLUDE_DIRECTORIES + "$;$") + +_install_target(AsyncExtensions) +set_property(GLOBAL APPEND PROPERTY ASYNC_EXTENSIONS_EXPORTS AsyncExtensions) diff --git a/cmake/modules/AsyncExtensionsConfig.cmake.in b/cmake/modules/AsyncExtensionsConfig.cmake.in new file mode 100644 index 0000000..2815eb3 --- /dev/null +++ b/cmake/modules/AsyncExtensionsConfig.cmake.in @@ -0,0 +1,21 @@ +#[[ +This source file is part of the AsyncExtensions open source project + +Copyright (c) 2024 The AsyncExtensions project authors +Licensed under MIT License + +See https://github.com/sideeffect-io/AsyncExtensions/blob/main/LICENSE for license information +#]] + +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}") + +find_dependency(SwiftCollections) +find_dependency(SwiftAtomics) + +include("${CMAKE_CURRENT_LIST_DIR}/AsyncExtensionsTargets.cmake") + +check_required_components(AsyncExtensions) diff --git a/cmake/modules/CMakeLists.txt b/cmake/modules/CMakeLists.txt new file mode 100644 index 0000000..6809ebb --- /dev/null +++ b/cmake/modules/CMakeLists.txt @@ -0,0 +1,38 @@ +#[[ +This source file is part of the AsyncExtensions open source project + +Copyright (c) 2024 The AsyncExtensions project authors +Licensed under MIT License + +See https://github.com/sideeffect-io/AsyncExtensions/blob/main/LICENSE for license information +#]] + +include(CMakePackageConfigHelpers) + +set(ASYNC_EXTENSIONS_EXPORTS_FILE + ${CMAKE_CURRENT_BINARY_DIR}/AsyncExtensionsExports.cmake) + +configure_file(AsyncExtensionsConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/AsyncExtensionsConfig.cmake) + +get_property(ASYNC_EXTENSIONS_EXPORTS GLOBAL PROPERTY ASYNC_EXTENSIONS_EXPORTS) +export(TARGETS ${ASYNC_EXTENSIONS_EXPORTS} + NAMESPACE AsyncExtensions:: + FILE ${ASYNC_EXTENSIONS_EXPORTS_FILE} + EXPORT_LINK_INTERFACE_LIBRARIES) + +install(EXPORT AsyncExtensionsTargets + FILE AsyncExtensionsTargets.cmake + NAMESPACE AsyncExtensions:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/AsyncExtensions) + +configure_package_config_file( + AsyncExtensionsConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/install/AsyncExtensionsConfig.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/AsyncExtensions) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/install/AsyncExtensionsConfig.cmake + FindSwiftCollections.cmake + FindSwiftAtomics.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/AsyncExtensions) diff --git a/cmake/modules/FindSwiftAtomics.cmake b/cmake/modules/FindSwiftAtomics.cmake new file mode 100644 index 0000000..e24965d --- /dev/null +++ b/cmake/modules/FindSwiftAtomics.cmake @@ -0,0 +1,54 @@ +#[[ +This source file is part of the AsyncExtensions open source project + +Copyright (c) 2024 The AsyncExtensions project authors +Licensed under MIT License + +See https://github.com/sideeffect-io/AsyncExtensions/blob/main/LICENSE for license information +#]] + +# FindSwiftAtomics.cmake +# +# Locates an installed apple/swift-atomics package built with its upstream +# CMake. Upstream installs `libAtomics.so` and the swiftmodule but does NOT +# install the `_AtomicsShims` headers / module.modulemap that `import Atomics` +# transitively imports at compile time. The prefix must have those staged +# under `include/_AtomicsShims/`. + +if(CMAKE_SYSTEM_NAME STREQUAL Darwin) + set(_swift_os macosx) +else() + string(TOLOWER "${CMAKE_SYSTEM_NAME}" _swift_os) +endif() + +find_library(SwiftAtomics_LIBRARY + NAMES Atomics + PATH_SUFFIXES lib/swift/${_swift_os} lib) + +find_path(SwiftAtomics_MODULE_DIR + NAMES Atomics.swiftmodule + PATH_SUFFIXES lib/swift/${_swift_os}) + +find_path(SwiftAtomics_SHIMS_INCLUDE_DIR + NAMES _AtomicsShims/module.modulemap + PATH_SUFFIXES include) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SwiftAtomics + REQUIRED_VARS + SwiftAtomics_LIBRARY + SwiftAtomics_MODULE_DIR + SwiftAtomics_SHIMS_INCLUDE_DIR) + +if(SwiftAtomics_FOUND AND NOT TARGET SwiftAtomics::Atomics) + add_library(SwiftAtomics::Atomics SHARED IMPORTED) + set_target_properties(SwiftAtomics::Atomics PROPERTIES + IMPORTED_LOCATION "${SwiftAtomics_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES + "${SwiftAtomics_MODULE_DIR};${SwiftAtomics_SHIMS_INCLUDE_DIR}") +endif() + +mark_as_advanced( + SwiftAtomics_LIBRARY + SwiftAtomics_MODULE_DIR + SwiftAtomics_SHIMS_INCLUDE_DIR) diff --git a/cmake/modules/FindSwiftCollections.cmake b/cmake/modules/FindSwiftCollections.cmake new file mode 100644 index 0000000..2a397d2 --- /dev/null +++ b/cmake/modules/FindSwiftCollections.cmake @@ -0,0 +1,56 @@ +#[[ +This source file is part of the AsyncExtensions open source project + +Copyright (c) 2024 The AsyncExtensions project authors +Licensed under MIT License + +See https://github.com/sideeffect-io/AsyncExtensions/blob/main/LICENSE for license information +#]] + +# FindSwiftCollections.cmake +# +# Locates an installed apple/swift-collections package built and installed with +# its upstream CMake build. + +if(CMAKE_SYSTEM_NAME STREQUAL Darwin) + set(_swift_os macosx) +else() + string(TOLOWER "${CMAKE_SYSTEM_NAME}" _swift_os) +endif() + +find_library(SwiftCollections_LIBRARY + NAMES Collections + PATH_SUFFIXES lib/swift/${_swift_os} lib) + +find_path(SwiftCollections_MODULE_DIR + NAMES Collections.swiftmodule + PATH_SUFFIXES lib/swift/${_swift_os}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SwiftCollections + REQUIRED_VARS + SwiftCollections_LIBRARY + SwiftCollections_MODULE_DIR) + +if(SwiftCollections_FOUND AND NOT TARGET SwiftCollections::Collections) + add_library(SwiftCollections::Collections SHARED IMPORTED) + set_target_properties(SwiftCollections::Collections PROPERTIES + IMPORTED_LOCATION "${SwiftCollections_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${SwiftCollections_MODULE_DIR}") + + foreach(_mod DequeModule BitCollections HashTreeCollections HeapModule + OrderedCollections InternalCollectionsUtilities) + find_library(SwiftCollections_${_mod}_LIBRARY + NAMES ${_mod} + PATH_SUFFIXES lib/swift/${_swift_os} lib) + if(SwiftCollections_${_mod}_LIBRARY) + add_library(SwiftCollections::${_mod} SHARED IMPORTED) + set_target_properties(SwiftCollections::${_mod} PROPERTIES + IMPORTED_LOCATION "${SwiftCollections_${_mod}_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${SwiftCollections_MODULE_DIR}") + mark_as_advanced(SwiftCollections_${_mod}_LIBRARY) + endif() + endforeach() +endif() + +mark_as_advanced(SwiftCollections_LIBRARY SwiftCollections_MODULE_DIR) diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake new file mode 100644 index 0000000..0549324 --- /dev/null +++ b/cmake/modules/SwiftSupport.cmake @@ -0,0 +1,98 @@ +#[[ +This source file is derived from the Swift System open source project + +Copyright (c) 2020 Apple Inc. and the Swift System project authors +Licensed under Apache License v2.0 with Runtime Library Exception + +See https://swift.org/LICENSE.txt for license information + +Modifications copyright (c) 2024 The AsyncExtensions project authors +#]] + +function(get_swift_host_arch result_var_name) + if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64") + set("${result_var_name}" "x86_64" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" MATCHES "AArch64|aarch64|arm64|ARM64") + if(CMAKE_SYSTEM_NAME MATCHES Darwin) + set("${result_var_name}" "arm64" PARENT_SCOPE) + else() + set("${result_var_name}" "aarch64" PARENT_SCOPE) + endif() + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64") + set("${result_var_name}" "powerpc64" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64le") + set("${result_var_name}" "powerpc64le" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "s390x") + set("${result_var_name}" "s390x" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv6l") + set("${result_var_name}" "armv6" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv7l") + set("${result_var_name}" "armv7" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "amd64") + set("${result_var_name}" "x86_64" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "AMD64") + set("${result_var_name}" "x86_64" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "riscv64") + set("${result_var_name}" "riscv64" PARENT_SCOPE) + else() + message(FATAL_ERROR "Unrecognized architecture on host system: ${CMAKE_SYSTEM_PROCESSOR}") + endif() +endfunction() + +function(get_swift_host_os result_var_name) + if(CMAKE_SYSTEM_NAME STREQUAL Darwin) + set(${result_var_name} macosx PARENT_SCOPE) + else() + string(TOLOWER ${CMAKE_SYSTEM_NAME} cmake_system_name_lc) + set(${result_var_name} ${cmake_system_name_lc} PARENT_SCOPE) + endif() +endfunction() + +if(NOT Swift_MODULE_TRIPLE) + set(module_triple_command "${CMAKE_Swift_COMPILER}" -print-target-info) + if(CMAKE_Swift_COMPILER_TARGET) + list(APPEND module_triple_command -target ${CMAKE_Swift_COMPILER_TARGET}) + endif() + execute_process(COMMAND ${module_triple_command} + OUTPUT_VARIABLE target_info_json) + string(JSON module_triple GET "${target_info_json}" "target" "moduleTriple") + + if(NOT module_triple) + message(FATAL_ERROR + "Failed to get module triple from Swift compiler. " + "Compiler output: ${target_info_json}") + endif() + + set(Swift_MODULE_TRIPLE "${module_triple}" CACHE STRING + "swift module triple used for installed swiftmodule and swiftinterface files") + mark_as_advanced(Swift_MODULE_TRIPLE) +endif() + +function(_install_target module) + get_swift_host_os(swift_os) + get_target_property(type ${module} TYPE) + + if(type STREQUAL STATIC_LIBRARY) + set(swift swift_static) + else() + set(swift swift) + endif() + + install(TARGETS ${module} + EXPORT AsyncExtensionsTargets) + if(type STREQUAL EXECUTABLE) + return() + endif() + + get_target_property(module_name ${module} Swift_MODULE_NAME) + if(NOT module_name) + set(module_name ${module}) + endif() + + install(FILES $/${module_name}.swiftdoc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/${swift}/${swift_os}/${module_name}.swiftmodule + RENAME ${Swift_MODULE_TRIPLE}.swiftdoc) + install(FILES $/${module_name}.swiftmodule + DESTINATION ${CMAKE_INSTALL_LIBDIR}/${swift}/${swift_os}/${module_name}.swiftmodule + RENAME ${Swift_MODULE_TRIPLE}.swiftmodule) +endfunction()