diff --git a/.gitignore b/.gitignore index bb460e7..59e2947 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ xcuserdata/ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index e378654..0000000 --- a/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections.git", - "state": { - "branch": null, - "revision": "f504716c27d2e5d4144fa4794b12129301d17729", - "version": "1.0.3" - } - } - ] - }, - "version": 1 -} diff --git a/Package.swift b/Package.swift index 4f5d569..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 @@ -16,23 +16,31 @@ let package = Package( name: "AsyncExtensions", targets: ["AsyncExtensions"]), ], - dependencies: [.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.3"))], + dependencies: [ + .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.3")), + .package(url: "https://github.com/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", - dependencies: ["AsyncExtensions"], - path: "Tests"), + dependencies: [ + "AsyncExtensions", + .product(name: "OpenCombine", package: "OpenCombine", condition: .when(platforms: [.linux])), + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms") + ], + path: "Tests", + swiftSettings: [.swiftLanguageMode(.v5)] + ), ] ) diff --git a/README.md b/README.md index c5e63a4..ef9c82d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ **AsyncExtensions** provides a collection of operators that intends to ease the creation and combination of `AsyncSequences`. -**AsyncExtensions** can be seen as a companion to Apple [swift-async-algorithms](https://github.com/apple/swift-async-algorithms). For now there is an overlap between both libraries, but when **swift-async-algorithms** becomes stable the overlapping operators while be deprecated in **AsyncExtensions**. Nevertheless **AsyncExtensions** will continue to provide the operators that the community needs and are not provided by Apple. +**AsyncExtensions** can be seen as a companion to Apple [swift-async-algorithms](https://github.com/apple/swift-async-algorithms), which provides operators that the community needs and are not provided by Apple. ## Adding AsyncExtensions as a Dependency @@ -44,11 +44,6 @@ AsyncStream) * [AsyncThrowingReplaySubject](./Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift): Throwing subject with a shared output. 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 +53,6 @@ AsyncStream) * [AsyncFailSequence](./Sources/Creators/AsyncFailSequence.swift): Creates an `AsyncSequence` that immediately fails * [AsyncJustSequence](./Sources/Creators/AsyncJustSequence.swift): Creates an `AsyncSequence` that emits an element an finishes * [AsyncThrowingJustSequence](./Sources/Creators/AsyncThrowingJustSequence.swift): Creates an `AsyncSequence` that emits an elements and finishes bases on a throwing closure -* [AsyncLazySequence](./Sources/Creators/AsyncLazySequence.swift): Creates an `AsyncSequence` of the elements from the base sequence * [AsyncTimerSequence](./Sources/Creators/AsyncTimerSequence.swift): Creates an `AsyncSequence` that emits a date value periodically * [AsyncStream Pipe](./Sources/Creators/AsyncStream+Pipe.swift): Creates an AsyncStream and returns a tuple standing for its inputs and outputs diff --git a/Sources/AsyncChannels/AsyncBufferedChannel.swift b/Sources/AsyncChannels/AsyncBufferedChannel.swift index 2f28efe..5b38dd3 100644 --- a/Sources/AsyncChannels/AsyncBufferedChannel.swift +++ b/Sources/AsyncChannels/AsyncBufferedChannel.swift @@ -5,6 +5,7 @@ // Created by Thibault Wittemberg on 07/01/2022. // +import Atomics import DequeModule import OrderedCollections @@ -69,7 +70,7 @@ public final class AsyncBufferedChannel: AsyncSequence, Senda enum State: @unchecked Sendable { case idle case queued(Deque) - case awaiting(OrderedSet) + case awaiting([Awaiting]) case finished static var initial: State { @@ -77,19 +78,16 @@ public final class AsyncBufferedChannel: AsyncSequence, 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 { @@ -184,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: @@ -200,12 +204,13 @@ 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)) + 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 073f0b7..b3579f6 100644 --- a/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift +++ b/Sources/AsyncChannels/AsyncThrowingBufferedChannel.swift @@ -5,6 +5,7 @@ // Created by Thibault Wittemberg on 07/01/2022. // +import Atomics import DequeModule import OrderedCollections @@ -80,7 +81,7 @@ public final class AsyncThrowingBufferedChannel: AsyncS enum State: @unchecked Sendable { case idle case queued(Deque) - case awaiting(OrderedSet) + case awaiting([Awaiting]) case terminated(Termination) static var initial: State { @@ -88,19 +89,16 @@ public final class AsyncThrowingBufferedChannel: AsyncS } } - let ids: ManagedCriticalState + let ids: ManagedAtomic let state: ManagedCriticalState public init() { - self.ids = ManagedCriticalState(0) + self.ids = ManagedAtomic(0) self.state = ManagedCriticalState(.initial) } func generateId() -> Int { - self.ids.withCriticalRegion { ids in - ids += 1 - return ids - } + ids.wrappingIncrementThenLoad(by: 1, ordering: .relaxed) } var hasBufferedElements: Bool { @@ -176,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 { @@ -208,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): @@ -227,12 +231,13 @@ 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)) + 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/AsyncSubjects/AsyncCurrentValueSubject.swift b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift index 5225105..5a14728 100644 --- a/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncCurrentValueSubject.swift @@ -67,54 +67,57 @@ public final class AsyncCurrentValueSubject: AsyncSubject where Element /// Sends a value to all consumers /// - Parameter element: the value to send public func send(_ element: Element) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncBufferedChannel] in state.current = element - for channel in state.channels.values { - channel.send(element) - } + return Array(state.channels.values) + } + + for channel in channels { + channel.send(element) } } /// Finishes the async sequences with a normal ending. /// - Parameter termination: The termination to finish the subject. public func send(_ termination: Termination) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncBufferedChannel] in state.terminalState = termination let channels = Array(state.channels.values) state.channels.removeAll() - for channel in channels { - channel.finish() - } + return channels + } + + for channel in channels { + channel.finish() } } func handleNewConsumer() -> (iterator: AsyncBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncBufferedChannel() + var consumerId: Int! + var unregister: (@Sendable () -> Void)? - let (terminalState, current) = self.state.withCriticalRegion { state -> (Termination?, Element) in - (state.terminalState, state.current) - } - - if let terminalState = terminalState, terminalState.isFinished { - asyncBufferedChannel.finish() - return (asyncBufferedChannel.makeAsyncIterator(), {}) - } - - asyncBufferedChannel.send(current) - - let consumerId = self.state.withCriticalRegion { state -> Int in - state.ids += 1 - state.channels[state.ids] = asyncBufferedChannel - return state.ids + self.state.withCriticalRegion { state in + let terminalState = state.terminalState + if let terminalState, terminalState.isFinished { + asyncBufferedChannel.finish() + } else { + state.ids &+= 1 + consumerId = state.ids + state.channels[consumerId] = asyncBufferedChannel + asyncBufferedChannel.send(state.current) + } } - let unregister = { @Sendable [state] in - state.withCriticalRegion { state in - state.channels[consumerId] = nil + if let consumerId { + unregister = { @Sendable [state, consumerId] in + state.withCriticalRegion { state in + state.channels[consumerId] = nil + } } } - return (asyncBufferedChannel.makeAsyncIterator(), unregister) + return (asyncBufferedChannel.makeAsyncIterator(), unregister ?? {}) } public func makeAsyncIterator() -> AsyncIterator { @@ -124,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() @@ -134,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 2badeb9..fc26774 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() } } @@ -106,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() @@ -116,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 f4e610e..b79b48f 100644 --- a/Sources/AsyncSubjects/AsyncReplaySubject.swift +++ b/Sources/AsyncSubjects/AsyncReplaySubject.swift @@ -46,36 +46,40 @@ public final class AsyncReplaySubject: AsyncSubject where Element: Send /// Sends a value to all consumers /// - Parameter element: the value to send public func send(_ element: Element) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncBufferedChannel] in if state.buffer.count >= state.bufferSize && !state.buffer.isEmpty { state.buffer.removeFirst() } state.buffer.append(element) - for channel in state.channels.values { - channel.send(element) - } + return Array(state.channels.values) + } + + for channel in channels { + channel.send(element) } } /// Finishes the subject with a normal ending. /// - Parameter termination: The termination to finish the subject. public func send(_ termination: Termination) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncBufferedChannel] in state.terminalState = termination let channels = Array(state.channels.values) state.channels.removeAll() state.buffer.removeAll() state.bufferSize = 0 - for channel in channels { - channel.finish() - } + return channels + } + + for channel in channels { + channel.finish() } } func handleNewConsumer() -> (iterator: AsyncBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncBufferedChannel() - let (terminalState, elements) = self.state.withCriticalRegion { state -> (Termination?, [Element]) in + let (terminalState, elements) = self.state.withCriticalRegion { state in (state.terminalState, state.buffer) } @@ -110,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() @@ -120,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 2294b09..04ba1d4 100644 --- a/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingCurrentValueSubject.swift @@ -67,28 +67,32 @@ public final class AsyncThrowingCurrentValueSubject: As /// Sends a value to all consumers /// - Parameter element: the value to send public func send(_ element: Element) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncThrowingBufferedChannel] in state.current = element - for channel in state.channels.values { - channel.send(element) - } + return Array(state.channels.values) + } + + for channel in channels { + channel.send(element) } } /// Finishes the subject with either a normal ending or an error. /// - Parameter termination: The termination to finish the subject. public func send(_ termination: Termination) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncThrowingBufferedChannel] in state.terminalState = termination let channels = Array(state.channels.values) state.channels.removeAll() - for channel in channels { - switch termination { - case .finished: - channel.finish() - case .failure(let error): - channel.fail(error) - } + return channels + } + + for channel in channels { + switch termination { + case .finished: + channel.finish() + case .failure(let error): + channel.fail(error) } } } @@ -97,8 +101,8 @@ public final class AsyncThrowingCurrentValueSubject: As ) -> (iterator: AsyncThrowingBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncThrowingBufferedChannel() - let (terminalState, current) = self.state.withCriticalRegion { state -> (Termination?, Element) in - (state.terminalState, state.current) + let terminalState = self.state.withCriticalRegion { state -> Termination? in + state.terminalState } if let terminalState = terminalState { @@ -111,11 +115,10 @@ public final class AsyncThrowingCurrentValueSubject: As return (asyncBufferedChannel.makeAsyncIterator(), {}) } - asyncBufferedChannel.send(current) - let consumerId = self.state.withCriticalRegion { state -> Int in state.ids += 1 state.channels[state.ids] = asyncBufferedChannel + asyncBufferedChannel.send(state.current) return state.ids } @@ -135,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() @@ -145,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 c1da4a5..b4ee14d 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) } } } @@ -119,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() @@ -129,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 c736d49..d0105d2 100644 --- a/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift +++ b/Sources/AsyncSubjects/AsyncThrowingReplaySubject.swift @@ -45,33 +45,37 @@ public final class AsyncThrowingReplaySubject: AsyncSub /// Sends a value to all consumers /// - Parameter element: the value to send public func send(_ element: Element) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncThrowingBufferedChannel] in if state.buffer.count >= state.bufferSize && !state.buffer.isEmpty { state.buffer.removeFirst() } state.buffer.append(element) - for channel in state.channels.values { - channel.send(element) - } + return Array(state.channels.values) + } + + for channel in channels { + channel.send(element) } } /// Finishes the subject with either a normal ending or an error. /// - Parameter termination: The termination to finish the subject public func send(_ termination: Termination) { - self.state.withCriticalRegion { state in + let channels = self.state.withCriticalRegion { state -> [AsyncThrowingBufferedChannel] in state.terminalState = termination let channels = Array(state.channels.values) state.channels.removeAll() state.buffer.removeAll() state.bufferSize = 0 - for channel in channels { - switch termination { - case .finished: - channel.finish() - case .failure(let error): - channel.fail(error) - } + return channels + } + + for channel in channels { + switch termination { + case .finished: + channel.finish() + case .failure(let error): + channel.fail(error) } } } @@ -80,7 +84,7 @@ public final class AsyncThrowingReplaySubject: AsyncSub ) -> (iterator: AsyncThrowingBufferedChannel.Iterator, unregister: @Sendable () -> Void) { let asyncBufferedChannel = AsyncThrowingBufferedChannel() - let (terminalState, elements) = self.state.withCriticalRegion { state -> (Termination?, [Element]) in + let (terminalState, elements) = self.state.withCriticalRegion { state in (state.terminalState, state.buffer) } @@ -120,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() @@ -130,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 } } } 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 index c152c53..ad85bf1 100644 --- a/Sources/Combiners/Merge/AsyncMergeSequence.swift +++ b/Sources/Combiners/Merge/AsyncMergeSequence.swift @@ -34,19 +34,15 @@ public struct AsyncMergeSequence: AsyncSequence { } 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): diff --git a/Sources/Combiners/Merge/MergeStateMachine.swift b/Sources/Combiners/Merge/MergeStateMachine.swift index 2f7984a..3affdc6 100644 --- a/Sources/Combiners/Merge/MergeStateMachine.swift +++ b/Sources/Combiners/Merge/MergeStateMachine.swift @@ -98,8 +98,7 @@ struct MergeStateMachine: Sendable { var regulators = [Regulator]() for base in bases { - let regulator = Regulator(base, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) } - ) + let regulator = Regulator(base, onNextRegulatedElement: { [state] in Self.onNextRegulatedElement($0, state: state) }) regulators.append(regulator) } @@ -238,7 +237,7 @@ struct MergeStateMachine: Sendable { } } - if case .element(.failure) = regulatedElement { + if case .termination = regulatedElement, case .element(.failure) = regulatedElement { self.task.cancel() } 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/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/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 { 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/Sources/Supporting/Locking.swift b/Sources/Supporting/Locking.swift new file mode 100644 index 0000000..d70a71b --- /dev/null +++ b/Sources/Supporting/Locking.swift @@ -0,0 +1,154 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(WinSDK) +import WinSDK +#elseif canImport(Android) +import Android +#endif + +internal struct Lock { +#if canImport(Darwin) + typealias Primitive = os_unfair_lock +#elseif canImport(Glibc) || canImport(Android) + typealias Primitive = pthread_mutex_t +#elseif canImport(WinSDK) + typealias Primitive = SRWLOCK +#endif + + typealias PlatformLock = UnsafeMutablePointer + let platformLock: PlatformLock + + private init(_ platformLock: PlatformLock) { + self.platformLock = platformLock + } + + fileprivate static func initialize(_ platformLock: PlatformLock) { +#if canImport(Darwin) + platformLock.initialize(to: os_unfair_lock()) +#elseif canImport(Glibc) || canImport(Android) + let result = pthread_mutex_init(platformLock, nil) + precondition(result == 0, "pthread_mutex_init failed") +#elseif canImport(WinSDK) + InitializeSRWLock(platformLock) +#endif + } + + fileprivate static func deinitialize(_ platformLock: PlatformLock) { +#if canImport(Glibc) || canImport(Android) + let result = pthread_mutex_destroy(platformLock) + precondition(result == 0, "pthread_mutex_destroy failed") +#endif + platformLock.deinitialize(count: 1) + } + + fileprivate static func lock(_ platformLock: PlatformLock) { +#if canImport(Darwin) + os_unfair_lock_lock(platformLock) +#elseif canImport(Glibc) || canImport(Android) + pthread_mutex_lock(platformLock) +#elseif canImport(WinSDK) + AcquireSRWLockExclusive(platformLock) +#endif + } + + fileprivate static func unlock(_ platformLock: PlatformLock) { +#if canImport(Darwin) + os_unfair_lock_unlock(platformLock) +#elseif canImport(Glibc) || canImport(Android) + let result = pthread_mutex_unlock(platformLock) + precondition(result == 0, "pthread_mutex_unlock failed") +#elseif canImport(WinSDK) + ReleaseSRWLockExclusive(platformLock) +#endif + } + + static func allocate() -> Lock { + let platformLock = PlatformLock.allocate(capacity: 1) + initialize(platformLock) + return Lock(platformLock) + } + + func deinitialize() { + Lock.deinitialize(platformLock) + } + + func lock() { + Lock.lock(platformLock) + } + + func unlock() { + Lock.unlock(platformLock) + } + + /// Acquire the lock for the duration of the given block. + /// + /// This convenience method should be preferred to `lock` and `unlock` in + /// most situations, as it ensures that the lock will be released regardless + /// of how `body` exits. + /// + /// - Parameter body: The block to execute while holding the lock. + /// - Returns: The value returned by the block. + func withLock(_ body: () throws -> T) rethrows -> T { + self.lock() + defer { + self.unlock() + } + return try body() + } + + // specialise Void return (for performance) + func withLockVoid(_ body: () throws -> Void) rethrows -> Void { + try self.withLock(body) + } +} + +struct ManagedCriticalState { + private final class LockedBuffer: ManagedBuffer { + deinit { + withUnsafeMutablePointerToElements { Lock.deinitialize($0) } + } + } + + private let buffer: ManagedBuffer + + init(_ initial: State) { + buffer = LockedBuffer.create(minimumCapacity: 1) { buffer in + buffer.withUnsafeMutablePointerToElements { Lock.initialize($0) } + return initial + } + } + + @discardableResult + func withCriticalRegion(_ critical: (inout State) throws -> R) rethrows -> R { + try buffer.withUnsafeMutablePointers { header, lock in + Lock.lock(lock) + defer { Lock.unlock(lock) } + return try critical(&header.pointee) + } + } + + func apply(criticalState newState: State) { + self.withCriticalRegion { actual in + actual = newState + } + } + + var criticalState: State { + self.withCriticalRegion { $0 } + } +} + +extension ManagedCriticalState: @unchecked Sendable where State: Sendable { } diff --git a/Sources/Supporting/ManagedCriticalState.swift b/Sources/Supporting/ManagedCriticalState.swift deleted file mode 100644 index 102b7d0..0000000 --- a/Sources/Supporting/ManagedCriticalState.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Darwin - -final class LockedBuffer: ManagedBuffer { - deinit { - _ = self.withUnsafeMutablePointerToElements { lock in - lock.deinitialize(count: 1) - } - } -} - -struct ManagedCriticalState { - let buffer: ManagedBuffer - - init(_ initial: State) { - buffer = LockedBuffer.create(minimumCapacity: 1) { buffer in - buffer.withUnsafeMutablePointerToElements { lock in - lock.initialize(to: os_unfair_lock()) - } - return initial - } - } - - @discardableResult - func withCriticalRegion( - _ critical: (inout State) throws -> R - ) rethrows -> R { - try buffer.withUnsafeMutablePointers { header, lock in - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - return try critical(&header.pointee) - } - } - - func apply(criticalState newState: State) { - self.withCriticalRegion { actual in - actual = newState - } - } - - var criticalState: State { - self.withCriticalRegion { $0 } - } -} - -extension ManagedCriticalState: @unchecked Sendable where State: Sendable { } diff --git a/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 02e6951..3decaf2 100644 --- a/Tests/AsyncSubjets/StreamedTests.swift +++ b/Tests/AsyncSubjets/StreamedTests.swift @@ -6,7 +6,11 @@ // import AsyncExtensions +#if canImport(Combine) import Combine +#elseif canImport(OpenCombine) +import OpenCombine +#endif import XCTest final class StreamedTests: XCTestCase { @@ -20,7 +24,7 @@ final class StreamedTests: XCTestCase { XCTAssertEqual(sut, newValue) } - func test_streamed_projects_in_asyncSequence() { + func test_streamed_projects_in_asyncSequence() async { let firstElementIsReceivedExpectation = expectation(description: "The first element has been received") let fifthElementIsReceivedExpectation = expectation(description: "The fifth element has been received") @@ -40,14 +44,14 @@ final class StreamedTests: XCTestCase { } } - wait(for: [firstElementIsReceivedExpectation], timeout: 1) + await fulfillment(of: [firstElementIsReceivedExpectation], timeout: 1) sut = 1 sut = 2 sut = 3 sut = 4 - wait(for: [fifthElementIsReceivedExpectation], timeout: 1) + await fulfillment(of: [fifthElementIsReceivedExpectation], timeout: 1) task.cancel() } } diff --git a/Tests/Combiners/Merge/AsyncMergeSequenceTests.swift b/Tests/Combiners/Merge/AsyncMergeSequenceTests.swift deleted file mode 100644 index 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/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/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/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 ffe9828..c3af0af 100644 --- a/Tests/Operators/AsyncMulticastSequenceTests.swift +++ b/Tests/Operators/AsyncMulticastSequenceTests.swift @@ -8,6 +8,27 @@ import AsyncExtensions import XCTest +private struct SpyAsyncSequenceForOnNextCall: AsyncSequence { + typealias Element = Element + typealias AsyncIterator = Iterator + + let onNext: () -> Void + + func makeAsyncIterator() -> AsyncIterator { + Iterator(onNext: self.onNext) + } + + struct Iterator: AsyncIteratorProtocol { + let onNext: () -> Void + + func next() async throws -> Element? { + self.onNext() + try await Task.sleep(nanoseconds: 100_000_000_000) + return nil + } + } +} + private class SpyAsyncSequenceForNumberOfIterators: AsyncSequence { typealias Element = Element typealias AsyncIterator = Iterator @@ -40,7 +61,7 @@ private class SpyAsyncSequenceForNumberOfIterators: AsyncSequence { } final class AsyncMulticastSequenceTests: XCTestCase { - func test_multiple_loops_receive_elements_from_single_baseIterator() { + func test_multiple_loops_receive_elements_from_single_baseIterator() async { let taskHaveIterators = expectation(description: "All tasks have their iterator") taskHaveIterators.expectedFulfillmentCount = 2 @@ -73,16 +94,16 @@ final class AsyncMulticastSequenceTests: XCTestCase { tasksHaveFinishedExpectation.fulfill() } - wait(for: [taskHaveIterators], timeout: 1) + await fulfillment(of: [taskHaveIterators], timeout: 1) sut.connect() - wait(for: [tasksHaveFinishedExpectation], timeout: 1) + await fulfillment(of: [tasksHaveFinishedExpectation], timeout: 1) XCTAssertEqual(spyUpstreamSequence.numberOfIterators, 1) } - func test_multiple_loops_uses_provided_stream() { + func test_multiple_loops_uses_provided_stream() async { let taskHaveIterators = expectation(description: "All tasks have their iterator") taskHaveIterators.expectedFulfillmentCount = 3 @@ -126,11 +147,11 @@ final class AsyncMulticastSequenceTests: XCTestCase { tasksHaveFinishedExpectation.fulfill() } - wait(for: [taskHaveIterators], timeout: 1) + await fulfillment(of: [taskHaveIterators], timeout: 1) sut.connect() - wait(for: [tasksHaveFinishedExpectation], timeout: 1) + await fulfillment(of: [tasksHaveFinishedExpectation], timeout: 1) XCTAssertEqual(spyUpstreamSequence.numberOfIterators, 1) } @@ -156,5 +177,46 @@ final class AsyncMulticastSequenceTests: XCTestCase { XCTAssertEqual(error as? MockError, expectedError) } } - + + func test_multicast_finishes_when_task_is_cancelled() async { + let taskHasFinishedExpectation = expectation(description: "Task has finished") + + let stream = AsyncThrowingPassthroughSubject() + let sut = [1, 2, 3, 4, 5] + .async + .multicast(stream) + .autoconnect() + + Task { + for try await _ in sut {} + taskHasFinishedExpectation.fulfill() + }.cancel() + + await fulfillment(of: [taskHasFinishedExpectation], timeout: 1) + } + + func test_multicast_finishes_when_task_is_cancelled_while_waiting_for_next() async { + let canCancelExpectation = expectation(description: "the task can be cancelled") + let taskHasFinishedExpectation = expectation(description: "Task has finished") + + let spyAsyncSequence = SpyAsyncSequenceForOnNextCall { + canCancelExpectation.fulfill() + } + + let stream = AsyncThrowingPassthroughSubject() + let sut = spyAsyncSequence + .multicast(stream) + .autoconnect() + + let task = Task { + for try await _ in sut {} + taskHasFinishedExpectation.fulfill() + } + + await fulfillment(of: [canCancelExpectation], timeout: 1) + + task.cancel() + + await fulfillment(of: [taskHasFinishedExpectation], timeout: 1) + } } diff --git a/Tests/Operators/AsyncPrependSequenceTests.swift b/Tests/Operators/AsyncPrependSequenceTests.swift index c23c03d..6e1836d 100644 --- a/Tests/Operators/AsyncPrependSequenceTests.swift +++ b/Tests/Operators/AsyncPrependSequenceTests.swift @@ -5,6 +5,7 @@ // Created by Thibault Wittemberg on 01/01/2022. // +import AsyncAlgorithms import AsyncExtensions import XCTest @@ -23,7 +24,7 @@ final class AsyncPrependSequenceTests: XCTestCase { XCTAssertEqual(receivedResult, expectedResult) } - func testPrepend_finishes_when_task_is_cancelled() { + func testPrepend_finishes_when_task_is_cancelled() async { let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExceptation = expectation(description: "The task has been cancelled") let taskHasFinishedExpectation = expectation(description: "The task has finished") @@ -43,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+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..f515aec 100644 --- a/Tests/Operators/AsyncSequence+FlatMapLatestTests.swift +++ b/Tests/Operators/AsyncSequence+FlatMapLatestTests.swift @@ -5,6 +5,7 @@ // Created by Thibault Wittemberg on 10/01/2022. // +import AsyncAlgorithms @testable import AsyncExtensions import XCTest @@ -166,10 +167,10 @@ final class AsyncSequence_FlatMapLatestTests: XCTestCase { func testFlatMapLatest_propagates_errors() async { let expectedError = MockError(code: Int.random(in: 0...100)) - let sut = AsyncLazySequence([1, 2]) + let sut = [1, 2].async .flatMapLatest { element -> AnyAsyncSequence in if element == 1 { - return AsyncLazySequence([1]).eraseToAnyAsyncSequence() + return [1].async.eraseToAnyAsyncSequence() } return AsyncFailSequence(expectedError).eraseToAnyAsyncSequence() @@ -183,7 +184,7 @@ final class AsyncSequence_FlatMapLatestTests: XCTestCase { } } - func testFlatMapLatest_finishes_when_task_is_cancelled_after_switched() { + func testFlatMapLatest_finishes_when_task_is_cancelled_after_switched() async { let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExceptation = expectation(description: "The task has been cancelled") let taskHasFinishedExpectation = expectation(description: "The task has finished") @@ -205,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 51b18d9..40d4502 100644 --- a/Tests/Operators/AsyncSequence+ShareTests.swift +++ b/Tests/Operators/AsyncSequence+ShareTests.swift @@ -47,7 +47,7 @@ private struct LongAsyncSequence: AsyncSequence, AsyncIteratorProtocol throw MockError(code: 0) } return self.elements.next() - } onCancel: {[onCancel] in + } onCancel: { [onCancel] in onCancel() } } @@ -58,7 +58,7 @@ private struct LongAsyncSequence: AsyncSequence, AsyncIteratorProtocol } final class AsyncSequence_ShareTests: XCTestCase { - func test_share_multicasts_values_to_clientLoops() { + func test_share_multicasts_values_to_clientLoops() async { let tasksHaveFinishedExpectation = expectation(description: "the tasks have finished") tasksHaveFinishedExpectation.expectedFulfillmentCount = 2 @@ -81,6 +81,6 @@ final class AsyncSequence_ShareTests: XCTestCase { tasksHaveFinishedExpectation.fulfill() } - waitForExpectations(timeout: 5) + await fulfillment(of: [tasksHaveFinishedExpectation], timeout: 5) } } diff --git a/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift b/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift index 8619cde..a6b87b6 100644 --- a/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift +++ b/Tests/Operators/AsyncSwitchToLatestSequenceTests.swift @@ -5,6 +5,7 @@ // Created by Thibault Wittemberg on 04/01/2022. // +import AsyncAlgorithms @testable import AsyncExtensions import XCTest @@ -102,10 +103,10 @@ final class AsyncSwitchToLatestSequenceTests: XCTestCase { func testSwitchToLatest_propagates_errors_when_base_sequence_fails() async { let sequences = [ - AsyncLazySequence([1, 2, 3]).eraseToAnyAsyncSequence(), - AsyncLazySequence([4, 5, 6]).eraseToAnyAsyncSequence(), - AsyncLazySequence([7, 8, 9]).eraseToAnyAsyncSequence(), // should fail here - AsyncLazySequence([10, 11, 12]).eraseToAnyAsyncSequence(), + [1, 2, 3].async.eraseToAnyAsyncSequence(), + [4, 5, 6].async.eraseToAnyAsyncSequence(), + [7, 8, 9].async.eraseToAnyAsyncSequence(), // should fail here + [10, 11, 12].async.eraseToAnyAsyncSequence(), ] let sourceSequence = LongAsyncSequence(elements: sequences, interval: .milliseconds(100), failAt: 2) @@ -151,7 +152,7 @@ final class AsyncSwitchToLatestSequenceTests: XCTestCase { } } - func testSwitchToLatest_finishes_when_task_is_cancelled_after_switched() { + func testSwitchToLatest_finishes_when_task_is_cancelled_after_switched() async { let canCancelExpectation = expectation(description: "The first element has been emitted") let hasCancelExceptation = expectation(description: "The task has been cancelled") let taskHasFinishedExpectation = expectation(description: "The task has finished") @@ -175,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 } }