diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 00000000..2c794bc9 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +builder: + configs: + - documentation_targets: [AsyncAlgorithms] + swift_version: 5.8 diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 00000000..3eb557ea --- /dev/null +++ b/.swiftformat @@ -0,0 +1,25 @@ +# file options + +--swiftversion 5.7 +--exclude .build + +# format options + +--self insert +--patternlet inline +--ranges nospace +--stripunusedargs unnamed-only +--ifdef no-indent +--extensionacl on-declarations +--disable typeSugar # https://github.com/nicklockwood/SwiftFormat/issues/636 +--disable andOperator +--disable wrapMultilineStatementBraces +--disable enumNamespaces +--disable redundantExtensionACL +--disable redundantReturn +--disable preferKeyPath +--disable sortedSwitchCases +--disable hoistAwait +--disable hoistTry + +# rules diff --git a/Evolution/0001-zip.md b/Evolution/0001-zip.md index 1ba48205..2e1885c5 100644 --- a/Evolution/0001-zip.md +++ b/Evolution/0001-zip.md @@ -2,9 +2,9 @@ * Proposal: [SAA-0001](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0001-zip.md) * Authors: [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** -* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncZip2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncZip3Sequence.swift) | +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestZip.swift)] * Decision Notes: * Bugs: @@ -45,8 +45,7 @@ public func zip: Sendable where Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element) public struct Iterator: AsyncIteratorProtocol { @@ -59,8 +58,7 @@ public struct AsyncZip2Sequence: Sen public struct AsyncZip3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable - Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element, Base3.Element) public struct Iterator: AsyncIteratorProtocol { diff --git a/Evolution/0002-merge.md b/Evolution/0002-merge.md index ff15f23d..b3dacfc5 100644 --- a/Evolution/0002-merge.md +++ b/Evolution/0002-merge.md @@ -2,9 +2,9 @@ * Proposal: [SAA-0002](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0002-merge.md) * Authors: [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** -* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Asyncmerge2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift) | +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestMerge.swift)] * Decision Notes: * Bugs: @@ -46,8 +46,7 @@ public struct AsyncMerge2Sequence: S where Base1.Element == Base2.Element, Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable { public typealias Element = Base1.Element public struct Iterator: AsyncIteratorProtocol { @@ -61,8 +60,7 @@ public struct AsyncMerge3Sequence: Sendable where Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element) public struct Iterator: AsyncIteratorProtocol { @@ -61,8 +60,7 @@ public struct AsyncCombineLatest2Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable - Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element, Base3.Element) public struct Iterator: AsyncIteratorProtocol { diff --git a/Evolution/0007-chain.md b/Evolution/0007-chain.md index 8df4804f..df2aa7c1 100644 --- a/Evolution/0007-chain.md +++ b/Evolution/0007-chain.md @@ -2,7 +2,7 @@ * Proposal: [SAA-0007](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0007-chain.md) * Authors: [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChain.swift)] diff --git a/Evolution/0008-bytes.md b/Evolution/0008-bytes.md index 76e2f3a0..50e2cf28 100644 --- a/Evolution/0008-bytes.md +++ b/Evolution/0008-bytes.md @@ -2,7 +2,7 @@ * Proposal: [SAA-0008](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0008-bytes.md) * Authors: [David Smith](https://github.com/Catfish-Man), [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift)] @@ -33,7 +33,7 @@ struct AsyncBytes: AsyncSequence { ## Detailed Design ```swift -public struct AsyncBufferedByteIterator: AsyncIteratorProtocol, Sendable { +public struct AsyncBufferedByteIterator: AsyncIteratorProtocol { public typealias Element = UInt8 public init( diff --git a/0009-async.md b/Evolution/0009-async.md similarity index 94% rename from 0009-async.md rename to Evolution/0009-async.md index 897e57b8..a29984ab 100644 --- a/0009-async.md +++ b/Evolution/0009-async.md @@ -1,6 +1,6 @@ # AsyncSyncSequence -* Proposal: [NNNN](NNNN-lazy.md) +* Proposal: [SAA-0009](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0009-async.md) * Authors: [Philippe Hausler](https://github.com/phausler) * Status: **Implemented** diff --git a/Evolution/0010-buffer.md b/Evolution/0010-buffer.md new file mode 100644 index 00000000..da56def7 --- /dev/null +++ b/Evolution/0010-buffer.md @@ -0,0 +1,114 @@ +# Buffer + +* Proposal: [SAA-0010](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0010-buffer.md) +* Author(s): [Thibault Wittemberg](https://github.com/twittemb) +* Status: **Accepted** +* Implementation: [ +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestBuffer.swift) +] +* Decision Notes: +* Bugs: + +## Introduction + +Buffering is a technique that balances supply and demand by temporarily storing elements to even out fluctuations in production and consumption rates. `AsyncStream` facilitates this process by allowing you to control the size of the buffer and the strategy for handling elements that exceed that size. However, this approach may not be suitable for all situations, and it doesn't provide a way to adapt other `AsyncSequence` types to incorporate buffering. + +This proposal presents a new type that addresses these more advanced requirements and offers a comprehensive solution for buffering in asynchronous sequences. + +## Motivation + +As an `AsyncSequence` operates as a pull system, the production of elements is directly tied to the demand expressed by the consumer. The slower the consumer is in requesting elements, the slower the production of these elements will be. This can negatively impact the software that produces the elements, as demonstrated in the following example. + +Consider an `AsyncSequence` that reads and returns a line from a file every time a new element is requested. To ensure exclusive access to the file, a lock is maintained while reading. Ideally, the lock should be held for as short a duration as possible to allow other processes to access the file. However, if the consumer is slow in processing received lines or has a fluctuating pace, the lock will be maintained for an extended period, reducing the performance of the system. + +To mitigate this issue, a buffer operator can be employed to streamline the consumption of the `AsyncSequence`. This operator allows an internal iteration of the `AsyncSequence` independently from the consumer. Each element would then be made available for consumption by using a queuing mechanism. + +By applying the buffer operator to the previous example, the file can be read as efficiently as possible, allowing the lock to be released in a timely manner. This results in improved performance, as the consumer can consume elements at its own pace without negatively affecting the system. The buffer operator ensures that the production of elements is not limited by the pace of the consumer, allowing both the producer and consumer to operate at optimal levels. + +## Proposed Solution + +We propose to extend `AsyncSequence` with a `buffer()` operator. This operator will return an `AsyncBuffereSequence` that wraps the source `AsyncSequence` and handle the buffering mechanism. + +This operator will accept an `AsyncBufferSequencePolicy`. The policy will dictate the behaviour in case of a buffer overflow. + +As of now we propose 4 different behaviours: + +```swift +public struct AsyncBufferSequencePolicy: Sendable { + public static func bounded(_ limit: Int) + public static var unbounded + public static func bufferingLatest(_ limit: Int) + public static func bufferingOldest(_ limit: Int) +} +``` + +And the public API of `AsyncBuffereSequence` will be: + +```swift +extension AsyncSequence where Self: Sendable { + public func buffer( + policy: AsyncBufferSequencePolicy + ) -> AsyncBufferSequence { + AsyncBufferSequence(base: self, policy: policy) + } +} + +public struct AsyncBufferSequence: AsyncSequence { + public typealias Element = Base.Element + + public func makeAsyncIterator() -> Iterator + + public struct Iterator: AsyncIteratorProtocol { + public mutating func next() async rethrows -> Element? + } +} + +extension AsyncBufferSequence: Sendable where Base: Sendable { } +``` + +## Notes on Sendable + +Since all buffering means that the base asynchronous sequence must be iterated independently of the consumption (to resolve the production versus consumption issue) the base `AsyncSequence` needs to be able to be sent across task boundaries (the iterator does not need this requirement). + +## Detailed Design + +The choice of the buffering policy is made through an enumeration whose values are related to a storage type. To date, two types of storage are implemented: + +- `BoundedBufferStorage` backing the `AsyncBufferSequencePolicy.bounded(_:)` policy, +- `UnboundedBufferStorage` backing the `AsyncBufferSequencePolicy.unbounded`, `AsyncBufferSequencePolicy.bufferingNewest(_:)` and `AsyncBufferSequencePolicy.bufferingLatest(_:)` policies. + +Both storage types rely on a Mealy state machine stored in a `ManagedCriticalState`. They drive the mutations of the internal buffer, while ensuring that concurrent access to the state are safe. + +### BoundedBufferStorage + +`BoundedBufferStorage` is instantiated with the upstream `AsyncSequence` and a buffer maximum size. Upon the first call to the `next()` method, a task is spawned to support the iteration over this `AsyncSequence`. The iteration retrieves elements and adds them to an internal buffer as long as the buffer limit has not been reached. Meanwhile, the downstream `AsyncSequence` can access and consume these elements from the buffer. If the rate of consumption is slower than the rate of production, the buffer will eventually become full. In this case, the iteration is temporarily suspended until additional space becomes available. + +### UnboundedBufferStorage + +`UnboundedBufferStorage` is instantiated with the upstream `AsyncSequence` and a buffering policy. Upon the first call to the `next()` method, a task is spawned to support the iteration over this `AsyncSequence`. + +From there the behaviour will depend on the buffering policy. + +#### Unbounded +If the policy is `unbounded`, the iteration retrieves elements and adds them to an internal buffer until the upstream `AsyncSequence` finishes or fails. Meanwhile, the downstream `AsyncSequence` can access and consume these elements from the buffer. + +#### BufferingLatest +If the policy is `bufferingLatest(_:)`, the iteration retrieves elements and adds them to an internal buffer. Meanwhile, the downstream `AsyncSequence` can access and consume these elements from the buffer. If the rate of consumption is slower than the rate of production, the buffer will eventually become full. In this case the oldest buffered elements will be removed from the buffer and the latest ones will be added. + +#### BufferingOldest +If the policy is `bufferingOldest(_:)`, the iteration retrieves elements and adds them to an internal buffer. Meanwhile, the downstream `AsyncSequence` can access and consume these elements from the buffer. If the rate of consumption is slower than the rate of production, the buffer will eventually become full. In this case the latest element will be discarded and never added to the buffer. + +### Terminal events and cancellation + +Terminal events from the upstream `AsyncSequence`, such as normal completion or failure, are delayed, ensuring that the consumer will receive all the elements before receiving the termination. + +If the consuming `Task` is cancelled, so will be the `Task` supporting the iteration of the upstream `AsyncSequence`. + +## Alternatives Considered + +The buffering mechanism was originally thought to rely on an open implementation of an `AsyncBuffer` protocol, which would be constrained to an `Actor` conformance. It was meant for developers to be able to provide their own implementation of a buffering algorithm. This buffer was designed to control the behavior of elements when pushed and popped, in an isolated manner. + +A default implementation was provided to offers a queuing strategy for buffering elements, with options for unbounded, oldest-first, or newest-first buffering. + +This implementation was eventually discarded due to the potentially higher cost of calling isolated functions on an `Actor`, compared to using a low-level locking mechanism like the one employed in other operators through the use of the `ManagedCriticalState`. diff --git a/Evolution/0011-interspersed.md b/Evolution/0011-interspersed.md new file mode 100644 index 00000000..cfc99737 --- /dev/null +++ b/Evolution/0011-interspersed.md @@ -0,0 +1,319 @@ +# Feature name + +* Proposal: [SAA-0011](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0011-interspersed.md) +* Authors: [Philippe Hausler](https://github.com/phausler) +* Review Manager: [Franz Busch](https://github.com/FranzBusch) +* Status: **Implemented** + +* Implementation: + [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift) | + [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestLazy.swift) + +## Motivation + +A common transformation that is applied to async sequences is to intersperse the elements with +a separator element. + +## Proposed solution + +We propose to add a new method on `AsyncSequence` that allows to intersperse +a separator between every n emitted element. This proposed API looks like this + +```swift +public extension AsyncSequence { + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: The value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: @Sendable @escaping () -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: @Sendable @escaping () async -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () async throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } +} +``` + +## Detailed design + +The bulk of the implementation of the new `interspersed` method is inside the new +`AsyncInterspersedSequence` struct. It constructs an iterator to the base async sequence +inside its own iterator. The `AsyncInterspersedSequence.Iterator.next()` is forwarding the demand +to the base iterator. + +Below is the implementation of the `AsyncInterspersedSequence`. +```swift +/// An asynchronous sequence that presents the elements of a base asynchronous sequence of +/// elements with a separator between each of those elements. +public struct AsyncInterspersedSequence { + @usableFromInline + internal enum Separator { + case element(Element) + case syncClosure(@Sendable () -> Element) + case asyncClosure(@Sendable () async -> Element) + } + + @usableFromInline + internal let base: Base + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal init(_ base: Base, every: Int, separator: Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .element(separator) + self.every = every + } + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .syncClosure(separator) + self.every = every + } + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .asyncClosure(separator) + self.every = every + } +} + +extension AsyncInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal var state = State.start(nil) + + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every + } + + public mutating func next() async rethrows -> Base.Element? { + // After the start, the state flips between element and separator. Before + // returning a separator, a check is made for the next element as a + // separator is only returned between two elements. The next element is + // stored to allow it to be returned in the next iteration. However, if + // the checking the next element throws, the separator is emitted before + // rethrowing that error. + switch state { + case var .start(element): + do { + if element == nil { + element = try await self.iterator.next() + } + + if let element = element { + if every == 1 { + state = .separator + } else { + state = .element(1) + } + return element + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case .separator: + do { + if let element = try await iterator.next() { + state = .start(element) + switch separator { + case let .element(element): + return element + + case let .syncClosure(closure): + return closure() + + case let .asyncClosure(closure): + return await closure() + } + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case let .element(count): + do { + if let element = try await iterator.next() { + let newCount = count + 1 + if every == newCount { + state = .separator + } else { + state = .element(newCount) + } + return element + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case .finished: + return nil + } + } + } + + @inlinable + public func makeAsyncIterator() -> AsyncInterspersedSequence.Iterator { + Iterator(base.makeAsyncIterator(), every: every, separator: separator) + } +} +``` diff --git a/Evolution/NNNN-channel.md b/Evolution/NNNN-channel.md new file mode 100644 index 00000000..09256190 --- /dev/null +++ b/Evolution/NNNN-channel.md @@ -0,0 +1,86 @@ +# Channel + +* Author(s): [Philippe Hausler](https://github.com/phausler) + +[ +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChannel.swift), +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift) | +[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChannel.swift) +] + +## Introduction + +`AsyncStream` introduced a mechanism to send buffered elements from a context that doesn't use Swift concurrency into one that does. That design only addressed a portion of the potential use cases; the missing portion was the back pressure excerpted across two concurrency domains. + +## Proposed Solution + +To achieve a system that supports back pressure and allows for the communication of more than one value from one task to another we are introducing a new type, the _channel_. The channel will be a reference-type asynchronous sequence with an asynchronous sending capability that awaits the consumption of iteration. Each value sent by the channel will await the consumption of that value by iteration. That awaiting behavior will allow for the affordance of back pressure applied from the consumption site to be transmitted to the production site. This means that the rate of production cannot exceed the rate of consumption, and that the rate of consumption cannot exceed the rate of production. Sending a terminal event to the channel will instantly resume all pending operations for every producers and consumers. + +## Detailed Design + +Similar to the `AsyncStream` and `AsyncThrowingStream` types, the type for sending elements via back pressure will come in two versions. These two versions will account for the throwing nature or non-throwing nature of the elements being produced. + +Each type will have functions to send elements and to send terminal events. + +```swift +public final class AsyncChannel: AsyncSequence, Sendable { + public struct Iterator: AsyncIteratorProtocol, Sendable { + public mutating func next() async -> Element? + } + + public init(element elementType: Element.Type = Element.self) + + public func send(_ element: Element) async + public func finish() + + public func makeAsyncIterator() -> Iterator +} + +public final class AsyncThrowingChannel: AsyncSequence, Sendable { + public struct Iterator: AsyncIteratorProtocol, Sendable { + public mutating func next() async throws -> Element? + } + + public init(element elementType: Element.Type = Element.self, failure failureType: Failure.Type = Failure.self) + + public func send(_ element: Element) async + public func fail(_ error: Error) where Failure == Error + public func finish() + + public func makeAsyncIterator() -> Iterator +} +``` + +Channels are intended to be used as communication types between tasks. Particularly when one task produces values and another task consumes said values. On the one hand, the back pressure applied by `send(_:)` via the suspension/resume ensures that the production of values does not exceed the consumption of values from iteration. This method suspends after enqueuing the event and is resumed when the next call to `next()` on the `Iterator` is made. On the other hand, the call to `finish()` or `fail(_:)` immediately resumes all the pending operations for every producers and consumers. Thus, every suspended `send(_:)` operations instantly resume, so as every suspended `next()` operations by producing a nil value, or by throwing an error, indicating the termination of the iterations. Further calls to `send(_:)` will immediately resume. The calls to `send(:)` and `next()` will immediately resume when their supporting task is cancelled, other operations from other tasks will remain active. + +```swift +let channel = AsyncChannel() +Task { + while let resultOfLongCalculation = doLongCalculations() { + await channel.send(resultOfLongCalculation) + } + channel.finish() +} + +for await calculationResult in channel { + print(calculationResult) +} +``` + +The example above uses a task to perform intense calculations; each of which are sent to the other task via the `send(_:)` method. That call to `send(_:)` returns when the next iteration of the channel is invoked. + +## Alternatives Considered + +The use of the name "subject" was considered, due to its heritage as a name for a sync-to-async adapter type. + +It was considered to make `AsyncChannel` and `AsyncThrowingChannel` actors, however due to the cancellation internals it would imply that these types would need to create new tasks to handle cancel events. The advantages of an actor in this particular case did not outweigh the impact of adjusting the implementations to be actors. + +## Future Directions + +`AsyncChannel` and `AsyncThrowingChannel` are just the prominent members of the channel-like behavior algorithms. It is reasonable to have as its own distinct type a buffering channel that provides more [tuned back pressure per a given buffer of elements](https://forums.swift.org/t/asyncchannel-should-we-allow-to-buffer/60876). These other members of the same category of algorithm should be considered on their own as distinct proposals. + +## Credits/Inspiration + +`AsyncChannel` and `AsyncThrowingChannel` was heavily inspired from `Subject` but with the key difference that it uses Swift concurrency to apply back pressure. + +https://developer.apple.com/documentation/combine/subject/ diff --git a/Package.swift b/Package.swift index 4f716a85..2932e199 100644 --- a/Package.swift +++ b/Package.swift @@ -1,9 +1,9 @@ -// swift-tools-version: 5.6 +// swift-tools-version: 5.8 import PackageDescription let package = Package( - name: "AsyncAlgorithms", + name: "swift-async-algorithms", platforms: [ .macOS("10.15"), .iOS("13.0"), @@ -12,40 +12,40 @@ let package = Package( ], products: [ .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), - .library(name: "AsyncSequenceValidation", targets: ["AsyncSequenceValidation"]), - .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]), - .library(name: "AsyncAlgorithms_XCTest", targets: ["AsyncAlgorithms_XCTest"]), ], - 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", from: "1.0.4"), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + ], targets: [ .target( name: "AsyncAlgorithms", - dependencies: [.product(name: "Collections", package: "swift-collections")] + dependencies: [.product(name: "Collections", package: "swift-collections")], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + ] ), .target( name: "AsyncSequenceValidation", - dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"]), + dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + ] + ), .systemLibrary(name: "_CAsyncSequenceValidationSupport"), .target( name: "AsyncAlgorithms_XCTest", - dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"]), + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + ] + ), .testTarget( name: "AsyncAlgorithmsTests", - dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"]), + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + ] + ), ] ) - -#if canImport(Darwin) -import Darwin -let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil -#elseif canImport(Glibc) -import Glibc -let buildingDocs = getenv("BUILDING_FOR_DOCUMENTATION_GENERATION") != nil -#else -let buildingDocs = false -#endif - -// Only require the docc plugin when building documentation -package.dependencies += buildingDocs ? [ - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), -] : [] diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift new file mode 100644 index 00000000..7c488af0 --- /dev/null +++ b/Package@swift-5.7.swift @@ -0,0 +1,39 @@ +// swift-tools-version: 5.6 + +import PackageDescription + +let package = Package( + name: "swift-async-algorithms", + platforms: [ + .macOS("10.15"), + .iOS("13.0"), + .tvOS("13.0"), + .watchOS("6.0") + ], + products: [ + .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), + .library(name: "AsyncSequenceValidation", targets: ["AsyncSequenceValidation"]), + .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]), + .library(name: "AsyncAlgorithms_XCTest", targets: ["AsyncAlgorithms_XCTest"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + ], + targets: [ + .target( + name: "AsyncAlgorithms", + dependencies: [.product(name: "Collections", package: "swift-collections")] + ), + .target( + name: "AsyncSequenceValidation", + dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"]), + .systemLibrary(name: "_CAsyncSequenceValidationSupport"), + .target( + name: "AsyncAlgorithms_XCTest", + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"]), + .testTarget( + name: "AsyncAlgorithmsTests", + dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"]), + ] +) diff --git a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift index 2a37f918..b1a0a156 100644 --- a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift @@ -9,6 +9,29 @@ // //===----------------------------------------------------------------------===// +extension AsyncSequence { + /// An `AsyncSequence` that iterates over the adjacent pairs of the original + /// original `AsyncSequence`. + /// + /// ``` + /// for await (first, second) in (1...5).async.adjacentPairs() { + /// print("First: \(first), Second: \(second)") + /// } + /// + /// // First: 1, Second: 2 + /// // First: 2, Second: 3 + /// // First: 3, Second: 4 + /// // First: 4, Second: 5 + /// ``` + /// + /// - Returns: An `AsyncSequence` where the element is a tuple of two adjacent elements + /// or the original `AsyncSequence`. + @inlinable + public func adjacentPairs() -> AsyncAdjacentPairsSequence { + AsyncAdjacentPairsSequence(self) + } +} + /// An `AsyncSequence` that iterates over the adjacent pairs of the original /// `AsyncSequence`. @frozen @@ -60,29 +83,6 @@ public struct AsyncAdjacentPairsSequence: AsyncSequence { } } -extension AsyncSequence { - /// An `AsyncSequence` that iterates over the adjacent pairs of the original - /// original `AsyncSequence`. - /// - /// ``` - /// for await (first, second) in (1...5).async.adjacentPairs() { - /// print("First: \(first), Second: \(second)") - /// } - /// - /// // First: 1, Second: 2 - /// // First: 2, Second: 3 - /// // First: 3, Second: 4 - /// // First: 4, Second: 5 - /// ``` - /// - /// - Returns: An `AsyncSequence` where the element is a tuple of two adjacent elements - /// or the original `AsyncSequence`. - @inlinable - public func adjacentPairs() -> AsyncAdjacentPairsSequence { - AsyncAdjacentPairsSequence(self) - } -} - extension AsyncAdjacentPairsSequence: Sendable where Base: Sendable, Base.Element: Sendable { } @available(*, unavailable) diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md index 19eed41c..dc2c2e66 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md @@ -3,8 +3,8 @@ * Author(s): [Philippe Hausler](https://github.com/phausler) [ -[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChannel.swift), -[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncThrowingChannel.swift) | +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift), +[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChannel.swift) ] diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md index 2ddacc5f..94389220 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chunked.md @@ -213,7 +213,7 @@ If both count and signal are specified, the chunking asynchronous sequence emits Like the example above, this code emits up to 1024-byte `Data` instances, but a chunk will also be emitted every second. ```swift -let packets = bytes.chunks(ofCount: 1024 or: .repeating(every: .seconds(1)), into: Data.self) +let packets = bytes.chunks(ofCount: 1024, or: .repeating(every: .seconds(1)), into: Data.self) for try await packet in packets { write(packet) } diff --git a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md index 8228a4e2..1419d68b 100644 --- a/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md +++ b/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md @@ -42,7 +42,7 @@ extension AsyncTimerSequence: Sendable { } extension AsyncTimerSequence.Iterator: Sendable { } ``` -Since all the types comprising `AsyncTimerSequence` and it's `Iterator` are `Sendable` these types are also `Sendable`. +Since all the types comprising `AsyncTimerSequence` are `Sendable` these types are also `Sendable`. ## Credits/Inspiration diff --git a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift index 1c275a2c..e88e3584 100644 --- a/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift @@ -95,3 +95,6 @@ extension AsyncChain3Sequence: AsyncSequence { } extension AsyncChain3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable { } + +@available(*, unavailable) +extension AsyncChain3Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift index 26faafe5..0ce5d199 100644 --- a/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncChunkedByGroupSequence.swift @@ -117,4 +117,6 @@ public struct AsyncChunkedByGroupSequence() -> AsyncCompactedSequence + where Element == Unwrapped? { + AsyncCompactedSequence(self) + } +} + /// An `AsyncSequence` that iterates over every non-nil element from the original /// `AsyncSequence`. @frozen @@ -50,22 +66,7 @@ public struct AsyncCompactedSequence: AsyncSequenc } } -extension AsyncSequence { - /// Returns a new `AsyncSequence` that iterates over every non-nil element from the - /// original `AsyncSequence`. - /// - /// Produces the same result as `c.compactMap { $0 }`. - /// - /// - Returns: An `AsyncSequence` where the element is the unwrapped original - /// element and iterates over every non-nil element from the original - /// `AsyncSequence`. - /// - /// Complexity: O(1) - @inlinable - public func compacted() -> AsyncCompactedSequence - where Element == Unwrapped? { - AsyncCompactedSequence(self) - } -} - extension AsyncCompactedSequence: Sendable where Base: Sendable, Base.Element: Sendable { } + +@available(*, unavailable) +extension AsyncCompactedSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift index 0d56c593..cef05359 100644 --- a/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncExclusiveReductionsSequence.swift @@ -112,3 +112,6 @@ extension AsyncExclusiveReductionsSequence: AsyncSequence { } extension AsyncExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable { } + +@available(*, unavailable) +extension AsyncExclusiveReductionsSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift index 12611c14..ca907b80 100644 --- a/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift @@ -58,7 +58,7 @@ extension AsyncInclusiveReductionsSequence: AsyncSequence { internal let transform: @Sendable (Base.Element, Base.Element) async -> Base.Element @inlinable - internal init( + init( _ iterator: Base.AsyncIterator, transform: @Sendable @escaping (Base.Element, Base.Element) async -> Base.Element ) { @@ -85,3 +85,6 @@ extension AsyncInclusiveReductionsSequence: AsyncSequence { } extension AsyncInclusiveReductionsSequence: Sendable where Base: Sendable { } + +@available(*, unavailable) +extension AsyncInclusiveReductionsSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift deleted file mode 100644 index 43f8d974..00000000 --- a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift +++ /dev/null @@ -1,102 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -extension AsyncSequence { - /// Returns an asynchronous sequence containing elements of this asynchronous sequence with - /// the given separator inserted in between each element. - /// - /// Any value of the asynchronous sequence's element type can be used as the separator. - /// - /// - Parameter separator: The value to insert in between each of this async - /// sequence’s elements. - /// - Returns: The interspersed asynchronous sequence of elements. - @inlinable - public func interspersed(with separator: Element) -> AsyncInterspersedSequence { - AsyncInterspersedSequence(self, separator: separator) - } -} - -/// An asynchronous sequence that presents the elements of a base asynchronous sequence of -/// elements with a separator between each of those elements. -public struct AsyncInterspersedSequence { - @usableFromInline - internal let base: Base - - @usableFromInline - internal let separator: Base.Element - - @usableFromInline - internal init(_ base: Base, separator: Base.Element) { - self.base = base - self.separator = separator - } -} - -extension AsyncInterspersedSequence: AsyncSequence { - public typealias Element = Base.Element - - /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. - public struct Iterator: AsyncIteratorProtocol { - @usableFromInline - internal enum State { - case start - case element(Result) - case separator - } - - @usableFromInline - internal var iterator: Base.AsyncIterator - - @usableFromInline - internal let separator: Base.Element - - @usableFromInline - internal var state = State.start - - @usableFromInline - internal init(_ iterator: Base.AsyncIterator, separator: Base.Element) { - self.iterator = iterator - self.separator = separator - } - - public mutating func next() async rethrows -> Base.Element? { - // After the start, the state flips between element and separator. Before - // returning a separator, a check is made for the next element as a - // separator is only returned between two elements. The next element is - // stored to allow it to be returned in the next iteration. However, if - // the checking the next element throws, the separator is emitted before - // rethrowing that error. - switch state { - case .start: - state = .separator - return try await iterator.next() - case .separator: - do { - guard let next = try await iterator.next() else { return nil } - state = .element(.success(next)) - } catch { - state = .element(.failure(error)) - } - return separator - case .element(let result): - state = .separator - return try result._rethrowGet() - } - } - } - - @inlinable - public func makeAsyncIterator() -> AsyncInterspersedSequence.Iterator { - Iterator(base.makeAsyncIterator(), separator: separator) - } -} - -extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift index e401eea2..515d8a8e 100644 --- a/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncJoinedBySeparatorSequence.swift @@ -142,3 +142,6 @@ public struct AsyncJoinedBySeparatorSequence: AsyncSequence where Base extension AsyncJoinedSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.Element.Element: Sendable { } + +@available(*, unavailable) +extension AsyncJoinedSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift index fd1d2192..0f45e21d 100644 --- a/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncRemoveDuplicatesSequence.swift @@ -143,3 +143,9 @@ public struct AsyncThrowingRemoveDuplicatesSequence: AsyncS extension AsyncRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable { } extension AsyncThrowingRemoveDuplicatesSequence: Sendable where Base: Sendable, Base.Element: Sendable { } + +@available(*, unavailable) +extension AsyncRemoveDuplicatesSequence.Iterator: Sendable { } + +@available(*, unavailable) +extension AsyncThrowingRemoveDuplicatesSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncSyncSequence.swift b/Sources/AsyncAlgorithms/AsyncSyncSequence.swift index 6710df7c..70a6637b 100644 --- a/Sources/AsyncAlgorithms/AsyncSyncSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncSyncSequence.swift @@ -67,3 +67,6 @@ public struct AsyncSyncSequence: AsyncSequence { } extension AsyncSyncSequence: Sendable where Base: Sendable { } + +@available(*, unavailable) +extension AsyncSyncSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift index 4832b9ac..4dbc1e48 100644 --- a/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift @@ -12,20 +12,20 @@ extension AsyncSequence { /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func throttle(for interval: C.Instant.Duration, clock: C, reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced) -> AsyncThrottleSequence { - AsyncThrottleSequence(self, interval: interval, clock: clock, reducing: reducing) + public func _throttle(for interval: C.Instant.Duration, clock: C, reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced) -> _AsyncThrottleSequence { + _AsyncThrottleSequence(self, interval: interval, clock: clock, reducing: reducing) } /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func throttle(for interval: Duration, reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced) -> AsyncThrottleSequence { - throttle(for: interval, clock: .continuous, reducing: reducing) + public func _throttle(for interval: Duration, reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced) -> _AsyncThrottleSequence { + _throttle(for: interval, clock: .continuous, reducing: reducing) } /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func throttle(for interval: C.Instant.Duration, clock: C, latest: Bool = true) -> AsyncThrottleSequence { - throttle(for: interval, clock: clock) { previous, element in + public func _throttle(for interval: C.Instant.Duration, clock: C, latest: Bool = true) -> _AsyncThrottleSequence { + _throttle(for: interval, clock: clock) { previous, element in if latest { return element } else { @@ -36,14 +36,14 @@ extension AsyncSequence { /// Create a rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func throttle(for interval: Duration, latest: Bool = true) -> AsyncThrottleSequence { - throttle(for: interval, clock: .continuous, latest: latest) + public func _throttle(for interval: Duration, latest: Bool = true) -> _AsyncThrottleSequence { + _throttle(for: interval, clock: .continuous, latest: latest) } } /// A rate-limited `AsyncSequence` by emitting values at most every specified interval. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct AsyncThrottleSequence { +public struct _AsyncThrottleSequence { let base: Base let interval: C.Instant.Duration let clock: C @@ -58,7 +58,7 @@ public struct AsyncThrottleSequence { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension AsyncThrottleSequence: AsyncSequence { +extension _AsyncThrottleSequence: AsyncSequence { public typealias Element = Reduced /// The iterator for an `AsyncThrottleSequence` instance. @@ -81,7 +81,16 @@ extension AsyncThrottleSequence: AsyncSequence { let start = last ?? clock.now repeat { guard let element = try await base.next() else { - return nil + if reduced != nil, let last { + // ensure the rate of elements never exceeds the given interval + let amount = interval - last.duration(to: clock.now) + if amount > .zero { + try? await clock.sleep(until: clock.now.advanced(by: amount), tolerance: nil) + } + } + // the last value is unable to have any subsequent + // values so always return the last reduction + return reduced } let reduction = await reducing(reduced, element) let now = clock.now @@ -101,4 +110,7 @@ extension AsyncThrottleSequence: AsyncSequence { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension AsyncThrottleSequence: Sendable where Base: Sendable, Element: Sendable { } +extension _AsyncThrottleSequence: Sendable where Base: Sendable, Element: Sendable { } + +@available(*, unavailable) +extension _AsyncThrottleSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift index 4b12c2c3..1cb49d8b 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingExclusiveReductionsSequence.swift @@ -119,3 +119,6 @@ extension AsyncThrowingExclusiveReductionsSequence: AsyncSequence { } extension AsyncThrowingExclusiveReductionsSequence: Sendable where Base: Sendable, Element: Sendable { } + +@available(*, unavailable) +extension AsyncThrowingExclusiveReductionsSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift index 36e88fb5..7779a842 100644 --- a/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift @@ -1,3 +1,14 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + extension AsyncSequence { /// Returns an asynchronous sequence containing the accumulated results of combining the /// elements of the asynchronous sequence using the given error-throwing closure. @@ -57,7 +68,7 @@ extension AsyncThrowingInclusiveReductionsSequence: AsyncSequence { internal let transform: @Sendable (Base.Element, Base.Element) async throws -> Base.Element @inlinable - internal init( + init( _ iterator: Base.AsyncIterator, transform: @Sendable @escaping (Base.Element, Base.Element) async throws -> Base.Element ) { @@ -89,3 +100,6 @@ extension AsyncThrowingInclusiveReductionsSequence: AsyncSequence { } extension AsyncThrowingInclusiveReductionsSequence: Sendable where Base: Sendable { } + +@available(*, unavailable) +extension AsyncThrowingInclusiveReductionsSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift index fe3b58f6..dcfc878b 100644 --- a/Sources/AsyncAlgorithms/AsyncTimerSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncTimerSequence.swift @@ -13,59 +13,49 @@ @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct AsyncTimerSequence: AsyncSequence { public typealias Element = C.Instant - + /// The iterator for an `AsyncTimerSequence` instance. public struct Iterator: AsyncIteratorProtocol { var clock: C? let interval: C.Instant.Duration let tolerance: C.Instant.Duration? var last: C.Instant? - + init(interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { self.clock = clock self.interval = interval self.tolerance = tolerance } - - func nextDeadline(_ clock: C) -> C.Instant { - let now = clock.now - let last = self.last ?? now - let next = last.advanced(by: interval) - if next < now { - return last.advanced(by: interval * Int(((next.duration(to: now)) / interval).rounded(.up))) - } else { - return next - } - } - + public mutating func next() async -> C.Instant? { - guard let clock = clock else { + guard let clock = self.clock else { return nil } - let next = nextDeadline(clock) + + let next = (self.last ?? clock.now).advanced(by: self.interval) do { - try await clock.sleep(until: next, tolerance: tolerance) + try await clock.sleep(until: next, tolerance: self.tolerance) } catch { self.clock = nil return nil } let now = clock.now - last = next + self.last = next return now } } - + let clock: C let interval: C.Instant.Duration let tolerance: C.Instant.Duration? - + /// Create an `AsyncTimerSequence` with a given repeating interval. public init(interval: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C) { self.clock = clock self.interval = interval self.tolerance = tolerance } - + public func makeAsyncIterator() -> Iterator { Iterator(interval: interval, tolerance: tolerance, clock: clock) } @@ -89,3 +79,6 @@ extension AsyncTimerSequence where C == SuspendingClock { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncTimerSequence: Sendable { } + +@available(*, unavailable) +extension AsyncTimerSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift index 8049a1a0..817615a4 100644 --- a/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift +++ b/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift @@ -70,7 +70,7 @@ public struct AsyncBufferSequencePolicy: Sendable { /// An `AsyncSequence` that buffers elements in regard to a policy. public struct AsyncBufferSequence: AsyncSequence { - enum StorageType { + enum StorageType { case transparent(Base.AsyncIterator) case bounded(storage: BoundedBufferStorage) case unbounded(storage: UnboundedBufferStorage) @@ -82,7 +82,7 @@ public struct AsyncBufferSequence: AsyncSequence let base: Base let policy: AsyncBufferSequencePolicy - public init( + init( base: Base, policy: AsyncBufferSequencePolicy ) { @@ -91,7 +91,7 @@ public struct AsyncBufferSequence: AsyncSequence } public func makeAsyncIterator() -> Iterator { - let storageType: StorageType + let storageType: StorageType switch self.policy.policy { case .bounded(...0), .bufferingNewest(...0), .bufferingOldest(...0): storageType = .transparent(self.base.makeAsyncIterator()) @@ -108,7 +108,7 @@ public struct AsyncBufferSequence: AsyncSequence } public struct Iterator: AsyncIteratorProtocol { - var storageType: StorageType + var storageType: StorageType public mutating func next() async rethrows -> Element? { switch self.storageType { diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift index a5c50a61..5c99d3d7 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStateMachine.swift @@ -9,14 +9,14 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule struct BoundedBufferStateMachine { typealias Element = Base.Element typealias SuspendedProducer = UnsafeContinuation typealias SuspendedConsumer = UnsafeContinuation?, Never> - private enum State { + fileprivate enum State { case initial(base: Base) case buffering( task: Task, @@ -64,7 +64,7 @@ struct BoundedBufferStateMachine { mutating func shouldSuspendProducer() -> Bool { switch state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(_, let buffer, .none, .none): // we are either idle or the buffer is already in use (no awaiting consumer) @@ -95,7 +95,7 @@ struct BoundedBufferStateMachine { mutating func producerSuspended(continuation: SuspendedProducer) -> ProducerSuspendedAction { switch self.state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(let task, let buffer, .none, .none): // we are either idle or the buffer is already in use (no awaiting consumer) @@ -132,7 +132,7 @@ struct BoundedBufferStateMachine { mutating func elementProduced(element: Element) -> ElementProducedAction { switch self.state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(let task, var buffer, .none, .none): // we are either idle or the buffer is already in use (no awaiting consumer) @@ -170,7 +170,7 @@ struct BoundedBufferStateMachine { mutating func finish(error: Error?) -> FinishAction { switch self.state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(_, var buffer, .none, .none): // we are either idle or the buffer is already in use (no awaiting consumer) @@ -245,7 +245,7 @@ struct BoundedBufferStateMachine { mutating func nextSuspended(continuation: SuspendedConsumer) -> NextSuspendedAction { switch self.state { case .initial: - preconditionFailure("Invalid state. The task should already by started.") + preconditionFailure("Invalid state. The task should already be started.") case .buffering(let task, let buffer, .none, .none) where buffer.isEmpty: // we are idle, we confirm the suspension of the consumer @@ -308,3 +308,6 @@ struct BoundedBufferStateMachine { } } } + +extension BoundedBufferStateMachine: Sendable where Base: Sendable { } +extension BoundedBufferStateMachine.State: Sendable where Base: Sendable { } diff --git a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift index c00360e1..f83a37fa 100644 --- a/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift +++ b/Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift @@ -18,34 +18,47 @@ final class BoundedBufferStorage: Sendable where Base: Send func next() async -> Result? { return await withTaskCancellationHandler { - let (shouldSuspend, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result?) in + let action: BoundedBufferStateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in let action = stateMachine.next() switch action { case .startTask(let base): self.startTask(stateMachine: &stateMachine, base: base) - return (true, nil) + return nil + case .suspend: - return (true, nil) - case .returnResult(let producerContinuation, let result): - producerContinuation?.resume() - return (false, result) + return action + case .returnResult: + return action } } - if !shouldSuspend { - return result + switch action { + case .startTask: + // We are handling the startTask in the lock already because we want to avoid + // other inputs interleaving while starting the task + fatalError("Internal inconsistency") + + case .suspend: + break + + case .returnResult(let producerContinuation, let result): + producerContinuation?.resume() + return result + + case .none: + break } return await withUnsafeContinuation { (continuation: UnsafeContinuation?, Never>) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.nextSuspended(continuation: continuation) - switch action { - case .none: - break - case .returnResult(let producerContinuation, let result): - producerContinuation?.resume() - continuation.resume(returning: result) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.nextSuspended(continuation: continuation) + } + switch action { + case .none: + break + case .returnResult(let producerContinuation, let result): + producerContinuation?.resume() + continuation.resume(returning: result) } } } onCancel: { @@ -68,15 +81,15 @@ final class BoundedBufferStorage: Sendable where Base: Send if shouldSuspend { await withUnsafeContinuation { (continuation: UnsafeContinuation) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.producerSuspended(continuation: continuation) - - switch action { - case .none: - break - case .resumeProducer: - continuation.resume() - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.producerSuspended(continuation: continuation) + } + + switch action { + case .none: + break + case .resumeProducer: + continuation.resume() } } } @@ -86,35 +99,35 @@ final class BoundedBufferStorage: Sendable where Base: Send break loop } - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced(element: element) - switch action { - case .none: - break - case .resumeConsumer(let continuation, let result): - continuation.resume(returning: result) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced(element: element) } - } - - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: nil) switch action { case .none: break - case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + case .resumeConsumer(let continuation, let result): + continuation.resume(returning: result) } } + + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: nil) + } + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) + } } catch { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: error) - switch action { - case .none: - break - case .resumeConsumer(let continuation): - continuation?.resume(returning: .failure(error)) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: error) + } + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: .failure(error)) } } } @@ -123,16 +136,16 @@ final class BoundedBufferStorage: Sendable where Base: Send } func interrupted() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.interrupted() - switch action { - case .none: - break - case .resumeProducerAndConsumer(let task, let producerContinuation, let consumerContinuation): - task.cancel() - producerContinuation?.resume() - consumerContinuation?.resume(returning: nil) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.interrupted() + } + switch action { + case .none: + break + case .resumeProducerAndConsumer(let task, let producerContinuation, let consumerContinuation): + task.cancel() + producerContinuation?.resume() + consumerContinuation?.resume(returning: nil) } } diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift index b163619a..de5d37ae 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStateMachine.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule struct UnboundedBufferStateMachine { typealias Element = Base.Element @@ -21,7 +21,7 @@ struct UnboundedBufferStateMachine { case bufferingOldest(Int) } - private enum State { + fileprivate enum State { case initial(base: Base) case buffering( task: Task, @@ -248,3 +248,6 @@ struct UnboundedBufferStateMachine { } } } + +extension UnboundedBufferStateMachine: Sendable where Base: Sendable { } +extension UnboundedBufferStateMachine.State: Sendable where Base: Sendable { } diff --git a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift index 59b02810..b63b261f 100644 --- a/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift +++ b/Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift @@ -19,32 +19,41 @@ final class UnboundedBufferStorage: Sendable where Base: Se func next() async -> Result? { return await withTaskCancellationHandler { - let (shouldSuspend, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result?) in + let action: UnboundedBufferStateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in let action = stateMachine.next() switch action { case .startTask(let base): self.startTask(stateMachine: &stateMachine, base: base) - return (true, nil) + return nil case .suspend: - return (true, nil) - case .returnResult(let result): - return (false, result) + return action + case .returnResult: + return action } } - if !shouldSuspend { - return result + switch action { + case .startTask: + // We are handling the startTask in the lock already because we want to avoid + // other inputs interleaving while starting the task + fatalError("Internal inconsistency") + case .suspend: + break + case .returnResult(let result): + return result + case .none: + break } return await withUnsafeContinuation { (continuation: UnsafeContinuation?, Never>) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.nextSuspended(continuation: continuation) - switch action { - case .none: - break - case .resumeConsumer(let result): - continuation.resume(returning: result) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.nextSuspended(continuation: continuation) + } + switch action { + case .none: + break + case .resumeConsumer(let result): + continuation.resume(returning: result) } } } onCancel: { @@ -59,35 +68,35 @@ final class UnboundedBufferStorage: Sendable where Base: Se let task = Task { do { for try await element in base { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced(element: element) - switch action { - case .none: - break - case .resumeConsumer(let continuation, let result): - continuation.resume(returning: result) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced(element: element) } - } - - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: nil) switch action { case .none: break - case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + case .resumeConsumer(let continuation, let result): + continuation.resume(returning: result) } } + + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: nil) + } + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) + } } catch { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: error) - switch action { - case .none: - break - case .resumeConsumer(let continuation): - continuation?.resume(returning: .failure(error)) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: error) + } + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: .failure(error)) } } } @@ -96,15 +105,15 @@ final class UnboundedBufferStorage: Sendable where Base: Se } func interrupted() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.interrupted() - switch action { - case .none: - break - case .resumeConsumer(let task, let continuation): - task.cancel() - continuation?.resume(returning: nil) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.interrupted() + } + switch action { + case .none: + break + case .resumeConsumer(let task, let continuation): + task.cancel() + continuation?.resume(returning: nil) } } diff --git a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift index 75becf2d..026281de 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncChannel.swift @@ -19,7 +19,7 @@ /// on the `Iterator` is made, or when `finish()` is called from another Task. /// As `finish()` induces a terminal state, there is no more need for a back pressure management. /// This function does not suspend and will finish all the pending iterations. -public final class AsyncChannel: AsyncSequence, @unchecked Sendable { +public final class AsyncChannel: AsyncSequence, Sendable { public typealias Element = Element public typealias AsyncIterator = Iterator @@ -58,3 +58,6 @@ public final class AsyncChannel: AsyncSequence, @unchecked Sendable { } } } + +@available(*, unavailable) +extension AsyncChannel.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift index 28de36ae..63cbf50d 100644 --- a/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift +++ b/Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift @@ -18,7 +18,7 @@ /// and is resumed when the next call to `next()` on the `Iterator` is made, or when `finish()`/`fail(_:)` is called /// from another Task. As `finish()` and `fail(_:)` induce a terminal state, there is no more need for a back pressure management. /// Those functions do not suspend and will finish all the pending iterations. -public final class AsyncThrowingChannel: AsyncSequence, @unchecked Sendable { +public final class AsyncThrowingChannel: AsyncSequence, Sendable { public typealias Element = Element public typealias AsyncIterator = Iterator @@ -61,3 +61,6 @@ public final class AsyncThrowingChannel: AsyncSequence, } } } + +@available(*, unavailable) +extension AsyncThrowingChannel.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift index 2972c754..2c8b1b92 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStateMachine.swift @@ -8,10 +8,13 @@ // See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// -@_implementationOnly import OrderedCollections +import OrderedCollections -struct ChannelStateMachine: Sendable { - private struct SuspendedProducer: Hashable { +// NOTE: this is only marked as unchecked since the swift-collections tag is before auditing for Sendable +extension OrderedSet: @unchecked Sendable where Element: Sendable { } + +struct ChannelStateMachine: Sendable { + private struct SuspendedProducer: Hashable, Sendable { let id: UInt64 let continuation: UnsafeContinuation? let element: Element? @@ -29,7 +32,7 @@ struct ChannelStateMachine: Sendable { } } - private struct SuspendedConsumer: Hashable { + private struct SuspendedConsumer: Hashable, Sendable { let id: UInt64 let continuation: UnsafeContinuation? @@ -51,7 +54,7 @@ struct ChannelStateMachine: Sendable { case failed(Error) } - private enum State { + private enum State: Sendable { case channeling( suspendedProducers: OrderedSet, cancelledProducers: Set, diff --git a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift index da398dbc..0fb67818 100644 --- a/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift +++ b/Sources/AsyncAlgorithms/Channels/ChannelStorage.swift @@ -8,7 +8,7 @@ // See https://swift.org/LICENSE.txt for license information // //===----------------------------------------------------------------------===// -struct ChannelStorage: Sendable { +struct ChannelStorage: Sendable { private let stateMachine: ManagedCriticalState> private let ids = ManagedCriticalState(0) @@ -25,21 +25,17 @@ struct ChannelStorage: Sendable { func send(element: Element) async { // check if a suspension is needed - let shouldExit = self.stateMachine.withCriticalRegion { stateMachine -> Bool in - let action = stateMachine.send() - - switch action { - case .suspend: - // the element has not been delivered because no consumer available, we must suspend - return false - case .resumeConsumer(let continuation): - continuation?.resume(returning: element) - return true - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.send() } - if shouldExit { - return + switch action { + case .suspend: + break + + case .resumeConsumer(let continuation): + continuation?.resume(returning: element) + return } let producerID = self.generateId() @@ -47,103 +43,100 @@ struct ChannelStorage: Sendable { await withTaskCancellationHandler { // a suspension is needed await withUnsafeContinuation { (continuation: UnsafeContinuation) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.sendSuspended(continuation: continuation, element: element, producerID: producerID) - - switch action { - case .none: - break - case .resumeProducer: - continuation.resume() - case .resumeProducerAndConsumer(let consumerContinuation): - continuation.resume() - consumerContinuation?.resume(returning: element) - } + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.sendSuspended(continuation: continuation, element: element, producerID: producerID) } - } - } onCancel: { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.sendCancelled(producerID: producerID) switch action { case .none: break - case .resumeProducer(let continuation): - continuation?.resume() + case .resumeProducer: + continuation.resume() + case .resumeProducerAndConsumer(let consumerContinuation): + continuation.resume() + consumerContinuation?.resume(returning: element) } } - } - } - - func finish(error: Failure? = nil) { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.finish(error: error) + } onCancel: { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.sendCancelled(producerID: producerID) + } switch action { case .none: break - case .resumeProducersAndConsumers(let producerContinuations, let consumerContinuations): - producerContinuations.forEach { $0?.resume() } - if let error { - consumerContinuations.forEach { $0?.resume(throwing: error) } - } else { - consumerContinuations.forEach { $0?.resume(returning: nil) } - } + case .resumeProducer(let continuation): + continuation?.resume() } } } - func next() async throws -> Element? { - let (shouldExit, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result?) in - let action = stateMachine.next() + func finish(error: Failure? = nil) { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.finish(error: error) + } - switch action { - case .suspend: - return (false, nil) - case .resumeProducer(let producerContinuation, let result): - producerContinuation?.resume() - return (true, result) - } + switch action { + case .none: + break + case .resumeProducersAndConsumers(let producerContinuations, let consumerContinuations): + producerContinuations.forEach { $0?.resume() } + if let error { + consumerContinuations.forEach { $0?.resume(throwing: error) } + } else { + consumerContinuations.forEach { $0?.resume(returning: nil) } + } } + } - if shouldExit { - return try result?._rethrowGet() + func next() async throws -> Element? { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.next() + } + + switch action { + case .suspend: + break + + case .resumeProducer(let producerContinuation, let result): + producerContinuation?.resume() + return try result._rethrowGet() } let consumerID = self.generateId() return try await withTaskCancellationHandler { try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.nextSuspended( + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.nextSuspended( continuation: continuation, consumerID: consumerID ) - - switch action { - case .none: - break - case .resumeConsumer(let element): - continuation.resume(returning: element) - case .resumeConsumerWithError(let error): - continuation.resume(throwing: error) - case .resumeProducerAndConsumer(let producerContinuation, let element): - producerContinuation?.resume() - continuation.resume(returning: element) - } } - } - } onCancel: { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.nextCancelled(consumerID: consumerID) switch action { case .none: break - case .resumeConsumer(let continuation): - continuation?.resume(returning: nil) + case .resumeConsumer(let element): + continuation.resume(returning: element) + case .resumeConsumerWithError(let error): + continuation.resume(throwing: error) + case .resumeProducerAndConsumer(let producerContinuation, let element): + producerContinuation?.resume() + continuation.resume(returning: element) } } + } onCancel: { + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.nextCancelled(consumerID: consumerID) + } + + switch action { + case .none: + break + case .resumeConsumer(let continuation): + continuation?.resume(returning: nil) + } } } } diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift index c07bd099..fa68acf7 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift @@ -46,7 +46,7 @@ public struct AsyncCombineLatest2Sequence< let base1: Base1 let base2: Base2 - public init(_ base1: Base1, _ base2: Base2) { + init(_ base1: Base1, _ base2: Base2) { self.base1 = base1 self.base2 = base2 } @@ -87,3 +87,6 @@ public struct AsyncCombineLatest2Sequence< } } } + +@available(*, unavailable) +extension AsyncCombineLatest2Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift index a1c7e51a..4353c0b0 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift @@ -97,3 +97,6 @@ public struct AsyncCombineLatest3Sequence< } } } + +@available(*, unavailable) +extension AsyncCombineLatest3Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift index 34f4dead..5217e8de 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStateMachine.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule /// State machine for combine latest struct CombineLatestStateMachine< @@ -530,7 +530,8 @@ struct CombineLatestStateMachine< preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") case .upstreamsFinished: - preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamThrew()") + // We need to tolerate multiple upstreams failing + return .none case .waitingForDemand(let task, let upstreams, _): // An upstream threw. We can cancel everything now and transition to finished. diff --git a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift index 31245579..0d97adea 100644 --- a/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift +++ b/Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift @@ -48,7 +48,7 @@ final class CombineLatestStorage< func next() async rethrows -> (Base1.Element, Base2.Element, Base3.Element?)? { try await withTaskCancellationHandler { let result = await withUnsafeContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in + let action: StateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in let action = stateMachine.next(for: continuation) switch action { case .startTask(let base1, let base2, let base3): @@ -58,47 +58,67 @@ final class CombineLatestStorage< base1: base1, base2: base2, base3: base3, - downStreamContinuation: continuation + downstreamContinuation: continuation ) + return nil - case .resumeContinuation(let downstreamContinuation, let result): - downstreamContinuation.resume(returning: result) + case .resumeContinuation: + return action - case .resumeUpstreamContinuations(let upstreamContinuations): - // bases can be iterated over for 1 iteration so their next value can be retrieved - upstreamContinuations.forEach { $0.resume() } + case .resumeUpstreamContinuations: + return action - case .resumeDownstreamContinuationWithNil(let continuation): - // the async sequence is already finished, immediately resuming - continuation.resume(returning: .success(nil)) + case .resumeDownstreamContinuationWithNil: + return action } } + + switch action { + case .startTask: + // We are handling the startTask in the lock already because we want to avoid + // other inputs interleaving while starting the task + fatalError("Internal inconsistency") + + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) + + case .resumeUpstreamContinuations(let upstreamContinuations): + // bases can be iterated over for 1 iteration so their next value can be retrieved + upstreamContinuations.forEach { $0.resume() } + + case .resumeDownstreamContinuationWithNil(let continuation): + // the async sequence is already finished, immediately resuming + continuation.resume(returning: .success(nil)) + + case .none: + break + } } return try result._rethrowGet() } onCancel: { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.cancelled() + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.cancelled() + } - switch action { - case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( - let downstreamContinuation, - let task, - let upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() + switch action { + case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let task, + let upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() - downstreamContinuation.resume(returning: .success(nil)) + downstreamContinuation.resume(returning: .success(nil)) - case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() - case .none: - break - } + case .none: + break } } } @@ -108,7 +128,7 @@ final class CombineLatestStorage< base1: Base1, base2: Base2, base3: Base3?, - downStreamContinuation: StateMachine.DownstreamContinuation + downstreamContinuation: StateMachine.DownstreamContinuation ) { // This creates a new `Task` that is iterating the upstream // sequences. We must store it to cancel it at the right times. @@ -124,33 +144,33 @@ final class CombineLatestStorage< // element from upstream. This continuation is only resumed // if the downstream consumer called `next` to signal his demand. try await withUnsafeThrowingContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.childTaskSuspended(baseIndex: 0, continuation: continuation) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.childTaskSuspended(baseIndex: 0, continuation: continuation) + } - switch action { - case .resumeContinuation(let upstreamContinuation): - upstreamContinuation.resume() + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() - case .resumeContinuationWithError(let upstreamContinuation, let error): - upstreamContinuation.resume(throwing: error) + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) - case .none: - break - } + case .none: + break } } if let element1 = try await base1Iterator.next() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced((element1, nil, nil)) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced((element1, nil, nil)) + } - switch action { - case .resumeContinuation(let downstreamContinuation, let result): - downstreamContinuation.resume(returning: result) + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) - case .none: - break - } + case .none: + break } } else { let action = self.stateMachine.withCriticalRegion { stateMachine in @@ -191,33 +211,33 @@ final class CombineLatestStorage< // element from upstream. This continuation is only resumed // if the downstream consumer called `next` to signal his demand. try await withUnsafeThrowingContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.childTaskSuspended(baseIndex: 1, continuation: continuation) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.childTaskSuspended(baseIndex: 1, continuation: continuation) + } - switch action { - case .resumeContinuation(let upstreamContinuation): - upstreamContinuation.resume() + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() - case .resumeContinuationWithError(let upstreamContinuation, let error): - upstreamContinuation.resume(throwing: error) + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) - case .none: - break - } + case .none: + break } } if let element2 = try await base1Iterator.next() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced((nil, element2, nil)) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced((nil, element2, nil)) + } - switch action { - case .resumeContinuation(let downstreamContinuation, let result): - downstreamContinuation.resume(returning: result) + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) - case .none: - break - } + case .none: + break } } else { let action = self.stateMachine.withCriticalRegion { stateMachine in @@ -259,33 +279,33 @@ final class CombineLatestStorage< // element from upstream. This continuation is only resumed // if the downstream consumer called `next` to signal his demand. try await withUnsafeThrowingContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.childTaskSuspended(baseIndex: 2, continuation: continuation) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.childTaskSuspended(baseIndex: 2, continuation: continuation) + } - switch action { - case .resumeContinuation(let upstreamContinuation): - upstreamContinuation.resume() + switch action { + case .resumeContinuation(let upstreamContinuation): + upstreamContinuation.resume() - case .resumeContinuationWithError(let upstreamContinuation, let error): - upstreamContinuation.resume(throwing: error) + case .resumeContinuationWithError(let upstreamContinuation, let error): + upstreamContinuation.resume(throwing: error) - case .none: - break - } + case .none: + break } } if let element3 = try await base1Iterator.next() { - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.elementProduced((nil, nil, element3)) + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.elementProduced((nil, nil, element3)) + } - switch action { - case .resumeContinuation(let downstreamContinuation, let result): - downstreamContinuation.resume(returning: result) + switch action { + case .resumeContinuation(let downstreamContinuation, let result): + downstreamContinuation.resume(returning: result) - case .none: - break - } + case .none: + break } } else { let action = self.stateMachine.withCriticalRegion { stateMachine in @@ -319,39 +339,38 @@ final class CombineLatestStorage< } } - do { - try await group.waitForAll() - } catch { - // One of the upstream sequences threw an error - self.stateMachine.withCriticalRegion { stateMachine in - let action = stateMachine.upstreamThrew(error) - - switch action { - case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( - let downstreamContinuation, - let error, - let task, - let upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - downstreamContinuation.resume(returning: .failure(error)) - - case .none: - break - } - } + while !group.isEmpty { + do { + try await group.next() + } catch { + // One of the upstream sequences threw an error + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.upstreamThrew(error) + } - group.cancelAll() + switch action { + case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + let downstreamContinuation, + let error, + let task, + let upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + task.cancel() + downstreamContinuation.resume(returning: .failure(error)) + case .none: + break + } + + group.cancelAll() + } } } } - stateMachine.taskIsStarted(task: task, downstreamContinuation: downStreamContinuation) + stateMachine.taskIsStarted(task: task, downstreamContinuation: downstreamContinuation) } } diff --git a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift index 5ae17f14..c57b2c42 100644 --- a/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift +++ b/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift @@ -13,14 +13,14 @@ extension AsyncSequence { /// Creates an asynchronous sequence that emits the latest element after a given quiescence period /// has elapsed by using a specified Clock. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func debounce(for interval: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C) -> AsyncDebounceSequence where Self: Sendable { + public func debounce(for interval: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C) -> AsyncDebounceSequence where Self: Sendable, Self.Element: Sendable { AsyncDebounceSequence(self, interval: interval, tolerance: tolerance, clock: clock) } /// Creates an asynchronous sequence that emits the latest element after a given quiescence period /// has elapsed. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public func debounce(for interval: Duration, tolerance: Duration? = nil) -> AsyncDebounceSequence where Self: Sendable { + public func debounce(for interval: Duration, tolerance: Duration? = nil) -> AsyncDebounceSequence where Self: Sendable, Self.Element: Sendable { self.debounce(for: interval, tolerance: tolerance, clock: .continuous) } } @@ -28,7 +28,7 @@ extension AsyncSequence { /// An `AsyncSequence` that emits the latest element after a given quiescence period /// has elapsed. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct AsyncDebounceSequence: Sendable where Base: Sendable { +public struct AsyncDebounceSequence: Sendable where Base.Element: Sendable { private let base: Base private let clock: C private let interval: C.Instant.Duration @@ -41,7 +41,7 @@ public struct AsyncDebounceSequence: Sendable whe /// - interval: The interval to debounce. /// - tolerance: The tolerance of the clock. /// - clock: The clock. - public init(_ base: Base, interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { + init(_ base: Base, interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { self.base = base self.interval = interval self.tolerance = tolerance @@ -53,20 +53,20 @@ public struct AsyncDebounceSequence: Sendable whe extension AsyncDebounceSequence: AsyncSequence { public typealias Element = Base.Element - public func makeAsyncIterator() -> AsyncIterator { + public func makeAsyncIterator() -> Iterator { let storage = DebounceStorage( base: self.base, interval: self.interval, tolerance: self.tolerance, clock: self.clock ) - return AsyncIterator(storage: storage) + return Iterator(storage: storage) } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension AsyncDebounceSequence { - public struct AsyncIterator: AsyncIteratorProtocol { + public struct Iterator: AsyncIteratorProtocol { /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. /// /// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncIterator`` struct itself. @@ -97,3 +97,6 @@ extension AsyncDebounceSequence { } } } + +@available(*, unavailable) +extension AsyncDebounceSequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift index bd111ab1..5fb89451 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStateMachine.swift @@ -10,10 +10,10 @@ //===----------------------------------------------------------------------===// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -struct DebounceStateMachine { +struct DebounceStateMachine: Sendable where Base.Element: Sendable { typealias Element = Base.Element - private enum State { + private enum State: Sendable { /// The initial state before a call to `next` happened. case initial(base: Base) @@ -390,8 +390,8 @@ struct DebounceStateMachine { preconditionFailure("Internal inconsistency current state \(self.state) and received upstreamFinished()") case .upstreamFailure: - // The upstream already failed so it should never have throw again. - preconditionFailure("Internal inconsistency current state \(self.state) and received childTaskSuspended()") + // We need to tolerate multiple upstreams failing + return .none case .waitingForDemand(let task, .none, let clockContinuation, .none): // We don't have any buffered element so we can just go ahead @@ -517,8 +517,8 @@ struct DebounceStateMachine { /// Actions returned by `clockSleepFinished()`. enum ClockSleepFinishedAction { /// Indicates that the downstream continuation should be resumed with the given element. - case resumeDownStreamContinuation( - downStreamContinuation: UnsafeContinuation, Never>, + case resumeDownstreamContinuation( + downstreamContinuation: UnsafeContinuation, Never>, element: Element ) } @@ -547,8 +547,8 @@ struct DebounceStateMachine { bufferedElement: nil ) - return .resumeDownStreamContinuation( - downStreamContinuation: downstreamContinuation, + return .resumeDownstreamContinuation( + downstreamContinuation: downstreamContinuation, element: currentElement.element ) } else { diff --git a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift index 21e1ddf6..1839e334 100644 --- a/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift +++ b/Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift @@ -10,7 +10,7 @@ //===----------------------------------------------------------------------===// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class DebounceStorage: @unchecked Sendable where Base: Sendable { +final class DebounceStorage: Sendable where Base.Element: Sendable { typealias Element = Base.Element /// The state machine protected with a lock. @@ -55,36 +55,59 @@ final class DebounceStorage: @unchecked Sendable // We always suspend since we can never return an element right away let result: Result = await withUnsafeContinuation { continuation in - self.stateMachine.withCriticalRegion { - let action = $0.next(for: continuation) - - switch action { - case .startTask(let base): - self.startTask( - stateMachine: &$0, - base: base, - downstreamContinuation: continuation - ) - - case .resumeUpstreamContinuation(let upstreamContinuation): - // This is signalling the upstream task that is consuming the upstream - // sequence to signal demand. - upstreamContinuation?.resume(returning: ()) - - case .resumeUpstreamAndClockContinuation(let upstreamContinuation, let clockContinuation, let deadline): - // This is signalling the upstream task that is consuming the upstream - // sequence to signal demand and start the clock task. - upstreamContinuation?.resume(returning: ()) - clockContinuation?.resume(returning: deadline) - - case .resumeDownstreamContinuationWithNil(let continuation): - continuation.resume(returning: .success(nil)) - - case .resumeDownstreamContinuationWithError(let continuation, let error): - continuation.resume(returning: .failure(error)) - } + let action: DebounceStateMachine.NextAction? = self.stateMachine.withCriticalRegion { + let action = $0.next(for: continuation) + + switch action { + case .startTask(let base): + self.startTask( + stateMachine: &$0, + base: base, + downstreamContinuation: continuation + ) + return nil + + case .resumeUpstreamContinuation: + return action + + case .resumeUpstreamAndClockContinuation: + return action + + case .resumeDownstreamContinuationWithNil: + return action + + case .resumeDownstreamContinuationWithError: + return action } - } + } + + switch action { + case .startTask: + // We are handling the startTask in the lock already because we want to avoid + // other inputs interleaving while starting the task + fatalError("Internal inconsistency") + + case .resumeUpstreamContinuation(let upstreamContinuation): + // This is signalling the upstream task that is consuming the upstream + // sequence to signal demand. + upstreamContinuation?.resume(returning: ()) + + case .resumeUpstreamAndClockContinuation(let upstreamContinuation, let clockContinuation, let deadline): + // This is signalling the upstream task that is consuming the upstream + // sequence to signal demand and start the clock task. + upstreamContinuation?.resume(returning: ()) + clockContinuation?.resume(returning: deadline) + + case .resumeDownstreamContinuationWithNil(let continuation): + continuation.resume(returning: .success(nil)) + + case .resumeDownstreamContinuationWithError(let continuation, let error): + continuation.resume(returning: .failure(error)) + + case .none: + break + } + } return try result._rethrowGet() } onCancel: { @@ -238,8 +261,8 @@ final class DebounceStorage: @unchecked Sendable let action = self.stateMachine.withCriticalRegion { $0.clockSleepFinished() } switch action { - case .resumeDownStreamContinuation(let downStreamContinuation, let element): - downStreamContinuation.resume(returning: .success(element)) + case .resumeDownstreamContinuation(let downstreamContinuation, let element): + downstreamContinuation.resume(returning: .success(element)) case .none: break @@ -254,36 +277,39 @@ final class DebounceStorage: @unchecked Sendable } } - do { - try await group.waitForAll() - } catch { - // The upstream sequence threw an error - let action = self.stateMachine.withCriticalRegion { $0.upstreamThrew(error) } - - switch action { - case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( - let downstreamContinuation, - let error, - let task, - let upstreamContinuation, - let clockContinuation - ): - upstreamContinuation?.resume(throwing: CancellationError()) - clockContinuation?.resume(throwing: CancellationError()) - - task.cancel() - - downstreamContinuation.resume(returning: .failure(error)) - - case .cancelTaskAndClockContinuation( - let task, - let clockContinuation - ): - clockContinuation?.resume(throwing: CancellationError()) - task.cancel() - - case .none: - break + while !group.isEmpty { + do { + try await group.next() + } catch { + // One of the upstream sequences threw an error + let action = self.stateMachine.withCriticalRegion { stateMachine in + stateMachine.upstreamThrew(error) + } + + switch action { + case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation( + let downstreamContinuation, + let error, + let task, + let upstreamContinuation, + let clockContinuation + ): + upstreamContinuation?.resume(throwing: CancellationError()) + clockContinuation?.resume(throwing: CancellationError()) + + task.cancel() + + downstreamContinuation.resume(returning: .failure(error)) + + case .cancelTaskAndClockContinuation( + let task, + let clockContinuation + ): + clockContinuation?.resume(throwing: CancellationError()) + task.cancel() + case .none: + break + } } group.cancelAll() diff --git a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift new file mode 100644 index 00000000..9932e77e --- /dev/null +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -0,0 +1,444 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +extension AsyncSequence { + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: The value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: { "-" }) + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: { "-" }) + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () async -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: { "-" }) + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } + + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: { "-" }) + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () async throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } +} + +/// An asynchronous sequence that presents the elements of a base asynchronous sequence of +/// elements with a separator between each of those elements. +public struct AsyncInterspersedSequence { + @usableFromInline + internal enum Separator { + case element(Element) + case syncClosure(@Sendable () -> Element) + case asyncClosure(@Sendable () async -> Element) + } + + @usableFromInline + internal let base: Base + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal init(_ base: Base, every: Int, separator: Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .element(separator) + self.every = every + } + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .syncClosure(separator) + self.every = every + } + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .asyncClosure(separator) + self.every = every + } +} + +extension AsyncInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal var state = State.start(nil) + + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every + } + + public mutating func next() async rethrows -> Base.Element? { + switch self.state { + case .start(var element): + do { + if element == nil { + element = try await self.iterator.next() + } + + if let element = element { + if self.every == 1 { + self.state = .separator + } else { + self.state = .element(1) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .separator: + do { + if let element = try await iterator.next() { + self.state = .start(element) + switch self.separator { + case .element(let element): + return element + + case .syncClosure(let closure): + return closure() + + case .asyncClosure(let closure): + return await closure() + } + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .element(let count): + do { + if let element = try await iterator.next() { + let newCount = count + 1 + if self.every == newCount { + self.state = .separator + } else { + self.state = .element(newCount) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .finished: + return nil + } + } + } + + @inlinable + public func makeAsyncIterator() -> Iterator { + Iterator(self.base.makeAsyncIterator(), every: self.every, separator: self.separator) + } +} + +/// An asynchronous sequence that presents the elements of a base asynchronous sequence of +/// elements with a separator between each of those elements. +public struct AsyncThrowingInterspersedSequence { + @usableFromInline + internal enum Separator { + case syncClosure(@Sendable () throws -> Element) + case asyncClosure(@Sendable () async throws -> Element) + } + + @usableFromInline + internal let base: Base + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () throws -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .syncClosure(separator) + self.every = every + } + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async throws -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .asyncClosure(separator) + self.every = every + } +} + +extension AsyncThrowingInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal var state = State.start(nil) + + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every + } + + public mutating func next() async throws -> Base.Element? { + switch self.state { + case .start(var element): + do { + if element == nil { + element = try await self.iterator.next() + } + + if let element = element { + if self.every == 1 { + self.state = .separator + } else { + self.state = .element(1) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .separator: + do { + if let element = try await iterator.next() { + self.state = .start(element) + switch self.separator { + case .syncClosure(let closure): + return try closure() + + case .asyncClosure(let closure): + return try await closure() + } + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .element(let count): + do { + if let element = try await iterator.next() { + let newCount = count + 1 + if self.every == newCount { + self.state = .separator + } else { + self.state = .element(newCount) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .finished: + return nil + } + } + } + + @inlinable + public func makeAsyncIterator() -> Iterator { + Iterator(self.base.makeAsyncIterator(), every: self.every, separator: self.separator) + } +} + +extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} +extension AsyncInterspersedSequence.Separator: Sendable where Base: Sendable, Base.Element: Sendable {} + +extension AsyncThrowingInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} +extension AsyncThrowingInterspersedSequence.Separator: Sendable where Base: Sendable, Base.Element: Sendable {} + +@available(*, unavailable) +extension AsyncInterspersedSequence.Iterator: Sendable {} +@available(*, unavailable) +extension AsyncThrowingInterspersedSequence.Iterator: Sendable {} diff --git a/Sources/AsyncAlgorithms/Locking.swift b/Sources/AsyncAlgorithms/Locking.swift index 4e8e246c..952b13c8 100644 --- a/Sources/AsyncAlgorithms/Locking.swift +++ b/Sources/AsyncAlgorithms/Locking.swift @@ -10,11 +10,11 @@ //===----------------------------------------------------------------------===// #if canImport(Darwin) -@_implementationOnly import Darwin +import Darwin #elseif canImport(Glibc) -@_implementationOnly import Glibc +import Glibc #elseif canImport(WinSDK) -@_implementationOnly import WinSDK +import WinSDK #endif internal struct Lock { @@ -24,6 +24,8 @@ internal struct Lock { typealias Primitive = pthread_mutex_t #elseif canImport(WinSDK) typealias Primitive = SRWLOCK +#else + typealias Primitive = Int #endif typealias PlatformLock = UnsafeMutablePointer diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift index 2723d112..9f82ed98 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences public func merge(_ base1: Base1, _ base2: Base2) -> AsyncMerge2Sequence @@ -40,7 +40,7 @@ public struct AsyncMerge2Sequence< /// - Parameters: /// - base1: The first upstream ``Swift/AsyncSequence``. /// - base2: The second upstream ``Swift/AsyncSequence``. - public init( + init( _ base1: Base1, _ base2: Base2 ) { @@ -50,18 +50,18 @@ public struct AsyncMerge2Sequence< } extension AsyncMerge2Sequence: AsyncSequence { - public func makeAsyncIterator() -> AsyncIterator { + public func makeAsyncIterator() -> Iterator { let storage = MergeStorage( base1: base1, base2: base2, base3: nil ) - return AsyncIterator(storage: storage) + return Iterator(storage: storage) } } extension AsyncMerge2Sequence { - public struct AsyncIterator: AsyncIteratorProtocol { + public struct Iterator: AsyncIteratorProtocol { /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. /// /// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncIterator`` struct itself. @@ -92,3 +92,6 @@ extension AsyncMerge2Sequence { } } } + +@available(*, unavailable) +extension AsyncMerge2Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift index c2b54eb1..d5576694 100644 --- a/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift +++ b/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences public func merge< @@ -49,7 +49,7 @@ public struct AsyncMerge3Sequence< /// - base1: The first upstream ``Swift/AsyncSequence``. /// - base2: The second upstream ``Swift/AsyncSequence``. /// - base3: The third upstream ``Swift/AsyncSequence``. - public init( + init( _ base1: Base1, _ base2: Base2, _ base3: Base3 @@ -61,18 +61,18 @@ public struct AsyncMerge3Sequence< } extension AsyncMerge3Sequence: AsyncSequence { - public func makeAsyncIterator() -> AsyncIterator { + public func makeAsyncIterator() -> Iterator { let storage = MergeStorage( base1: base1, base2: base2, base3: base3 ) - return AsyncIterator(storage: storage) + return Iterator(storage: storage) } } public extension AsyncMerge3Sequence { - struct AsyncIterator: AsyncIteratorProtocol { + struct Iterator: AsyncIteratorProtocol { /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. /// /// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncIterator`` struct itself. @@ -103,3 +103,6 @@ public extension AsyncMerge3Sequence { } } } + +@available(*, unavailable) +extension AsyncMerge3Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift index e3bb59e4..bb832ada 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStateMachine.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -@_implementationOnly import DequeModule +import DequeModule /// The state machine for any of the `merge` operator. /// diff --git a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift index de4c72b8..9dedee76 100644 --- a/Sources/AsyncAlgorithms/Merge/MergeStorage.swift +++ b/Sources/AsyncAlgorithms/Merge/MergeStorage.swift @@ -147,303 +147,142 @@ final class MergeStorage< // sequences. We must store it to cancel it at the right times. let task = Task { await withThrowingTaskGroup(of: Void.self) { group in - // For each upstream sequence we are adding a child task that - // is consuming the upstream sequence - group.addTask { - var iterator1 = base1.makeAsyncIterator() - - // This is our upstream consumption loop - loop: while true { - // We are creating a continuation before requesting the next - // element from upstream. This continuation is only resumed - // if the downstream consumer called `next` to signal his demand. - try await withUnsafeThrowingContinuation { continuation in - let action = self.lock.withLock { - self.stateMachine.childTaskSuspended(continuation) - } - - switch action { - case let .resumeContinuation(continuation): - // This happens if there is outstanding demand - // and we need to demand from upstream right away - continuation.resume(returning: ()) - - case let .resumeContinuationWithError(continuation, error): - // This happens if another upstream already failed or if - // the task got cancelled. - continuation.resume(throwing: error) - - case .none: - break - } - } + self.iterateAsyncSequence(base1, in: &group) + self.iterateAsyncSequence(base2, in: &group) - // We got signalled from the downstream that we have demand so let's - // request a new element from the upstream - if let element1 = try await iterator1.next() { - let action = self.lock.withLock { - self.stateMachine.elementProduced(element1) - } - - switch action { - case let .resumeContinuation(continuation, element): - // We had an outstanding demand and where the first - // upstream to produce an element so we can forward it to - // the downstream - continuation.resume(returning: element) - - case .none: - break - } - - } else { - // The upstream returned `nil` which indicates that it finished - let action = self.lock.withLock { - self.stateMachine.upstreamFinished() - } - - // All of this is mostly cleanup around the Task and the outstanding - // continuations used for signalling. - switch action { - case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation, - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - downstreamContinuation.resume(returning: nil) - - break loop - - case let .cancelTaskAndUpstreamContinuations( - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - break loop - case .none: - - break loop - } + // Copy from the above just using the base3 sequence + if let base3 = base3 { + self.iterateAsyncSequence(base3, in: &group) + } + + while !group.isEmpty { + do { + try await group.next() + } catch { + // One of the upstream sequences threw an error + let action = self.lock.withLock { + self.stateMachine.upstreamThrew(error) + } + switch action { + case let .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + downstreamContinuation, + error, + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + + task.cancel() + + downstreamContinuation.resume(throwing: error) + case let .cancelTaskAndUpstreamContinuations( + task, + upstreamContinuations + ): + upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } + + task.cancel() + case .none: + break } + group.cancelAll() } } + } + } - // Copy from the above just using the base2 sequence - group.addTask { - var iterator2 = base2.makeAsyncIterator() - - // This is our upstream consumption loop - loop: while true { - // We are creating a continuation before requesting the next - // element from upstream. This continuation is only resumed - // if the downstream consumer called `next` to signal his demand. - try await withUnsafeThrowingContinuation { continuation in - let action = self.lock.withLock { - self.stateMachine.childTaskSuspended(continuation) - } - - switch action { - case let .resumeContinuation(continuation): - // This happens if there is outstanding demand - // and we need to demand from upstream right away - continuation.resume(returning: ()) - - case let .resumeContinuationWithError(continuation, error): - // This happens if another upstream already failed or if - // the task got cancelled. - continuation.resume(throwing: error) - - case .none: - break - } - } + // We need to inform our state machine that we started the Task + stateMachine.taskStarted(task) + } - // We got signalled from the downstream that we have demand so let's - // request a new element from the upstream - if let element2 = try await iterator2.next() { - let action = self.lock.withLock { - self.stateMachine.elementProduced(element2) - } - - switch action { - case let .resumeContinuation(continuation, element): - // We had an outstanding demand and where the first - // upstream to produce an element so we can forward it to - // the downstream - continuation.resume(returning: element) - - case .none: - break - } - - } else { - // The upstream returned `nil` which indicates that it finished - let action = self.lock.withLock { - self.stateMachine.upstreamFinished() - } - - // All of this is mostly cleanup around the Task and the outstanding - // continuations used for signalling. - switch action { - case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation, - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - downstreamContinuation.resume(returning: nil) - - break loop - - case let .cancelTaskAndUpstreamContinuations( - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - break loop - case .none: - - break loop - } - } + private func iterateAsyncSequence( + _ base: AsyncSequence, + in taskGroup: inout ThrowingTaskGroup + ) where AsyncSequence.Element == Base1.Element, AsyncSequence: Sendable { + // For each upstream sequence we are adding a child task that + // is consuming the upstream sequence + taskGroup.addTask { + var iterator = base.makeAsyncIterator() + + // This is our upstream consumption loop + loop: while true { + // We are creating a continuation before requesting the next + // element from upstream. This continuation is only resumed + // if the downstream consumer called `next` to signal his demand. + try await withUnsafeThrowingContinuation { continuation in + let action = self.lock.withLock { + self.stateMachine.childTaskSuspended(continuation) } - } - // Copy from the above just using the base3 sequence - if let base3 = base3 { - group.addTask { - var iterator3 = base3.makeAsyncIterator() - - // This is our upstream consumption loop - loop: while true { - // We are creating a continuation before requesting the next - // element from upstream. This continuation is only resumed - // if the downstream consumer called `next` to signal his demand. - try await withUnsafeThrowingContinuation { continuation in - let action = self.lock.withLock { - self.stateMachine.childTaskSuspended(continuation) - } - - switch action { - case let .resumeContinuation(continuation): - // This happens if there is outstanding demand - // and we need to demand from upstream right away - continuation.resume(returning: ()) - - case let .resumeContinuationWithError(continuation, error): - // This happens if another upstream already failed or if - // the task got cancelled. - continuation.resume(throwing: error) - - case .none: - break - } - } - - // We got signalled from the downstream that we have demand so let's - // request a new element from the upstream - if let element3 = try await iterator3.next() { - let action = self.lock.withLock { - self.stateMachine.elementProduced(element3) - } - - switch action { - case let .resumeContinuation(continuation, element): - // We had an outstanding demand and where the first - // upstream to produce an element so we can forward it to - // the downstream - continuation.resume(returning: element) - - case .none: - break - } - - } else { - // The upstream returned `nil` which indicates that it finished - let action = self.lock.withLock { - self.stateMachine.upstreamFinished() - } - - // All of this is mostly cleanup around the Task and the outstanding - // continuations used for signalling. - switch action { - case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( - downstreamContinuation, - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - downstreamContinuation.resume(returning: nil) - - break loop - - case let .cancelTaskAndUpstreamContinuations( - task, - upstreamContinuations - ): - upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - - break loop - case .none: - - break loop - } - } - } + switch action { + case let .resumeContinuation(continuation): + // This happens if there is outstanding demand + // and we need to demand from upstream right away + continuation.resume(returning: ()) + + case let .resumeContinuationWithError(continuation, error): + // This happens if another upstream already failed or if + // the task got cancelled. + continuation.resume(throwing: error) + + case .none: + break } } - do { - try await group.waitForAll() - } catch { - // One of the upstream sequences threw an error + // We got signalled from the downstream that we have demand so let's + // request a new element from the upstream + if let element1 = try await iterator.next() { + let action = self.lock.withLock { + self.stateMachine.elementProduced(element1) + } + + switch action { + case let .resumeContinuation(continuation, element): + // We had an outstanding demand and where the first + // upstream to produce an element so we can forward it to + // the downstream + continuation.resume(returning: element) + + case .none: + break + } + + } else { + // The upstream returned `nil` which indicates that it finished let action = self.lock.withLock { - self.stateMachine.upstreamThrew(error) + self.stateMachine.upstreamFinished() } + // All of this is mostly cleanup around the Task and the outstanding + // continuations used for signalling. switch action { - case let .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations( + case let .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations( downstreamContinuation, - error, task, upstreamContinuations ): upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() - downstreamContinuation.resume(throwing: error) + downstreamContinuation.resume(returning: nil) + + break loop + case let .cancelTaskAndUpstreamContinuations( task, upstreamContinuations ): upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) } - task.cancel() + break loop case .none: - break - } - group.cancelAll() + break loop + } } } } - - // We need to inform our state machine that we started the Task - stateMachine.taskStarted(task) } } - diff --git a/Sources/AsyncAlgorithms/PartialIteration.swift b/Sources/AsyncAlgorithms/PartialIteration.swift deleted file mode 100644 index 1404ddc2..00000000 --- a/Sources/AsyncAlgorithms/PartialIteration.swift +++ /dev/null @@ -1,47 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -enum PartialIteration: CustomStringConvertible { - case idle(Iterator) - case pending(Task) - case terminal - - var description: String { - switch self { - case .idle: return "idle" - case .pending: return "pending" - case .terminal: return "terminal" - } - } - - mutating func resolve(_ result: Result, _ iterator: Iterator) rethrows -> Iterator.Element? { - do { - guard let value = try result._rethrowGet() else { - self = .terminal - return nil - } - self = .idle(iterator) - return value - } catch { - self = .terminal - throw error - } - } - - mutating func cancel() { - if case .pending(let task) = self { - task.cancel() - } - self = .terminal - } -} - -extension PartialIteration: Sendable where Iterator: Sendable, Iterator.Element: Sendable { } diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift index 6fb341ac..34e42913 100644 --- a/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift @@ -69,3 +69,6 @@ public struct AsyncZip2Sequence: Asy } } } + +@available(*, unavailable) +extension AsyncZip2Sequence.Iterator: Sendable { } diff --git a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift index 87474bae..513dc27a 100644 --- a/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift +++ b/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift @@ -74,3 +74,6 @@ public struct AsyncZip3Sequence (Base1.Element, Base2.Element, Base3.Element?)? { try await withTaskCancellationHandler { let result = await withUnsafeContinuation { continuation in - self.stateMachine.withCriticalRegion { stateMachine in + let action: StateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in let action = stateMachine.next(for: continuation) switch action { case .startTask(let base1, let base2, let base3): @@ -49,44 +49,61 @@ final class ZipStorage UnownedSerialExecutor { + UnownedSerialExecutor(ordinary: self) + } + } + + private static let _executor = ClockExecutor() + + static var unownedExecutor: UnownedSerialExecutor { + _executor.asUnownedSerialExecutor() + } +#else + @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + final class ClockExecutor_5_9: SerialExecutor { + func enqueue(_ job: __owned ExecutorJob) { + job.runSynchronously(on: asUnownedSerialExecutor()) } func asUnownedSerialExecutor() -> UnownedSerialExecutor { @@ -77,9 +95,34 @@ extension AsyncSequenceValidationDiagram { } } + final class ClockExecutor_Pre5_9: SerialExecutor { + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(*, deprecated, message: "Implement 'enqueue(_: __owned ExecutorJob)' instead") + func enqueue(_ job: UnownedJob) { + job._runSynchronously(on: self.asUnownedSerialExecutor()) + } + + func asUnownedSerialExecutor() -> UnownedSerialExecutor { + UnownedSerialExecutor(ordinary: self) + } + } + + private static let _executor: AnyObject = { + if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { + return ClockExecutor_5_9() + } else { + return ClockExecutor_Pre5_9() + } + }() + + static var unownedExecutor: UnownedSerialExecutor { + (_executor as! any SerialExecutor).asUnownedSerialExecutor() + } +#endif + static var clock: Clock? - static let executor = ClockExecutor() + static var driver: TaskDriver? diff --git a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift new file mode 100644 index 00000000..e51a4817 --- /dev/null +++ b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift @@ -0,0 +1,181 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import AsyncAlgorithms +import XCTest + +final class TestInterspersed: XCTestCase { + func test_interspersed() async { + let source = [1, 2, 3, 4, 5] + let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] + let sequence = source.async.interspersed(with: 0) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_every() async { + let source = [1, 2, 3, 4, 5, 6, 7, 8] + let expected = [1, 2, 3, 0, 4, 5, 6, 0, 7, 8] + let sequence = source.async.interspersed(every: 3, with: 0) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_closure() async { + let source = [1, 2, 3, 4, 5] + let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] + let sequence = source.async.interspersed(with: { 0 }) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_async_closure() async { + let source = [1, 2, 3, 4, 5] + let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] + let sequence = source.async.interspersed { + try! await Task.sleep(nanoseconds: 1000) + return 0 + } + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_throwing_closure() async { + let source = [1, 2] + let expected = [1] + var actual = [Int]() + let sequence = source.async.interspersed(with: { throw Failure() }) + + var iterator = sequence.makeAsyncIterator() + do { + while let item = try await iterator.next() { + actual.append(item) + } + XCTFail() + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try! await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_async_throwing_closure() async { + let source = [1, 2] + let expected = [1] + var actual = [Int]() + let sequence = source.async.interspersed { + try await Task.sleep(nanoseconds: 1000) + throw Failure() + } + + var iterator = sequence.makeAsyncIterator() + do { + while let item = try await iterator.next() { + actual.append(item) + } + XCTFail() + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try! await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_empty() async { + let source = [Int]() + let expected = [Int]() + let sequence = source.async.interspersed(with: 0) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_with_throwing_upstream() async { + let source = [1, 2, 3, -1, 4, 5] + let expected = [1, 0, 2, 0, 3] + var actual = [Int]() + let sequence = source.async.map { + try throwOn(-1, $0) + }.interspersed(with: 0) + + var iterator = sequence.makeAsyncIterator() + do { + while let item = try await iterator.next() { + actual.append(item) + } + XCTFail() + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try! await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_cancellation() async { + let source = Indefinite(value: "test") + let sequence = source.async.interspersed(with: "sep") + let lockStepChannel = AsyncChannel() + + await withTaskGroup(of: Void.self) { group in + group.addTask { + var iterator = sequence.makeAsyncIterator() + let _ = await iterator.next() + + // Information the parent task that we are consuming + await lockStepChannel.send(()) + + while let _ = await iterator.next() {} + + await lockStepChannel.send(()) + } + + // Waiting until the child task started consuming + _ = await lockStepChannel.first { _ in true } + + // Now we cancel the child + group.cancelAll() + + await group.waitForAll() + } + } +} diff --git a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift index d8003ca8..4ea06e06 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift @@ -90,11 +90,12 @@ final class TestThroughput: XCTestCase { zip($0, $1, $2) } } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func test_debounce() async { + if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { await measureSequenceThroughput(source: (1...).async) { - $0.debounce(for: .zero, clock: ContinuousClock()) + $0.debounce(for: .zero, clock: ContinuousClock()) } + } } } #endif diff --git a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift index c8991bdd..db223f3e 100644 --- a/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift +++ b/Tests/AsyncAlgorithmsTests/Performance/ThroughputMeasurement.swift @@ -56,7 +56,7 @@ final class _ThroughputMetric: NSObject, XCTMetric, @unchecked Sendable { } extension XCTestCase { - public func measureChannelThroughput(output: @escaping @autoclosure () -> Output) async { + public func measureChannelThroughput(output: @Sendable @escaping @autoclosure () -> Output) async { let metric = _ThroughputMetric() let sampleTime: Double = 0.1 @@ -85,7 +85,7 @@ extension XCTestCase { } } - public func measureThrowingChannelThroughput(output: @escaping @autoclosure () -> Output) async { + public func measureThrowingChannelThroughput(output: @Sendable @escaping @autoclosure () -> Output) async { let metric = _ThroughputMetric() let sampleTime: Double = 0.1 diff --git a/Tests/AsyncAlgorithmsTests/Support/Asserts.swift b/Tests/AsyncAlgorithmsTests/Support/Asserts.swift index c9cdb968..d891cf91 100644 --- a/Tests/AsyncAlgorithmsTests/Support/Asserts.swift +++ b/Tests/AsyncAlgorithmsTests/Support/Asserts.swift @@ -165,3 +165,47 @@ internal func XCTAssertThrowsError( verify(error) } } + +class WaiterDelegate: NSObject, XCTWaiterDelegate { + let state: ManagedCriticalState?> = ManagedCriticalState(nil) + + init(_ continuation: UnsafeContinuation) { + state.withCriticalRegion { $0 = continuation } + } + + func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) { + resume() + } + + func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) { + resume() + } + + func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) { + resume() + } + + func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) { + + } + + func resume() { + let continuation = state.withCriticalRegion { continuation in + defer { continuation = nil } + return continuation + } + continuation?.resume() + } +} + +extension XCTestCase { + @_disfavoredOverload + func fulfillment(of expectations: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool = false, file: StaticString = #file, line: Int = #line) async { + return await withUnsafeContinuation { continuation in + let delegate = WaiterDelegate(continuation) + let waiter = XCTWaiter(delegate: delegate) + waiter.wait(for: expectations, timeout: timeout, enforceOrder: enforceOrder) + delegate.resume() + } + } +} diff --git a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift index 843624f6..4fea0ef5 100644 --- a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift +++ b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift @@ -88,10 +88,11 @@ final class TestAdjacentPairs: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestBuffer.swift b/Tests/AsyncAlgorithmsTests/TestBuffer.swift index f8ef8a3c..7bb45f89 100644 --- a/Tests/AsyncAlgorithmsTests/TestBuffer.swift +++ b/Tests/AsyncAlgorithmsTests/TestBuffer.swift @@ -190,13 +190,13 @@ final class TestBuffer: XCTestCase { finished.fulfill() } // ensure the task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // When task.cancel() // Then - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_given_a_base_sequence_when_buffering_with_bounded_then_the_buffer_is_filled_in_and_suspends() async { @@ -310,13 +310,13 @@ final class TestBuffer: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // When task.cancel() // Then - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_given_a_base_sequence_when_bounded_with_limit_0_then_the_policy_is_transparent() async { diff --git a/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift b/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift index ee6c5d12..2c8841d8 100644 --- a/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift +++ b/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift @@ -199,10 +199,10 @@ final class TestBufferedByteIterator: XCTestCase { } } } - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestChain.swift b/Tests/AsyncAlgorithmsTests/TestChain.swift index b3be90a3..6bb2cb55 100644 --- a/Tests/AsyncAlgorithmsTests/TestChain.swift +++ b/Tests/AsyncAlgorithmsTests/TestChain.swift @@ -87,13 +87,13 @@ final class TestChain2: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } @@ -192,12 +192,12 @@ final class TestChain3: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestChannel.swift b/Tests/AsyncAlgorithmsTests/TestChannel.swift index b181bddd..b5d28cd9 100644 --- a/Tests/AsyncAlgorithmsTests/TestChannel.swift +++ b/Tests/AsyncAlgorithmsTests/TestChannel.swift @@ -133,7 +133,7 @@ final class TestChannel: XCTestCase { task1.cancel() // Then: the first sending operation is resumed - wait(for: [send1IsResumed], timeout: 1.0) + await fulfillment(of: [send1IsResumed], timeout: 1.0) // When: collecting elements var iterator = sut.makeAsyncIterator() diff --git a/Tests/AsyncAlgorithmsTests/TestChunk.swift b/Tests/AsyncAlgorithmsTests/TestChunk.swift index 4970cdc0..7845b1a0 100644 --- a/Tests/AsyncAlgorithmsTests/TestChunk.swift +++ b/Tests/AsyncAlgorithmsTests/TestChunk.swift @@ -13,15 +13,25 @@ import XCTest import AsyncSequenceValidation import AsyncAlgorithms +@Sendable func sumCharacters(_ array: [String]) -> String { return "\(array.reduce(into: 0) { $0 = $0 + Int($1)! })" } +@Sendable func concatCharacters(_ array: [String]) -> String { return array.joined() } final class TestChunk: XCTestCase { + func test_count_one() { + validate { + "ABCDE|" + $0.inputs[0].chunks(ofCount: 1).map(concatCharacters) + "ABCDE|" + } + } + func test_signal_equalChunks() { validate { "ABC- DEF- GHI- |" diff --git a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift index f33258c9..cb0f7324 100644 --- a/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift +++ b/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift @@ -84,7 +84,7 @@ final class TestCombineLatest2: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a"), (2, "b"), (3, "b"), (3, "c")]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (2, "a"), (2, "b"), (3, "b"), (3, "c")]) } @@ -126,7 +126,7 @@ final class TestCombineLatest2: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c")]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c")]) } @@ -168,7 +168,7 @@ final class TestCombineLatest2: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (2, "a"), (3, "a"), (3, "b"), (3, "c")]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (2, "a"), (3, "a"), (3, "b"), (3, "c")]) } @@ -210,7 +210,7 @@ final class TestCombineLatest2: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a"), (1, "b"), (1, "c"), (2, "c"), (3, "c")]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (1, "b"), (1, "c"), (2, "c"), (3, "c")]) } @@ -250,7 +250,7 @@ final class TestCombineLatest2: XCTestCase { XCTAssertEqual(validator.failure as? Failure, Failure()) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (1, "b")]) } @@ -290,7 +290,7 @@ final class TestCombineLatest2: XCTestCase { XCTAssertEqual(validator.failure as? Failure, Failure()) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a"), (2, "a")]) } @@ -312,11 +312,11 @@ final class TestCombineLatest2: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_combineLatest_when_cancelled() async { @@ -389,7 +389,7 @@ final class TestCombineLatest3: XCTestCase { value = await validator.validate() XCTAssertEqual(value, [(1, "a", 4), (2, "a", 4), (2, "b", 4), (2, "b", 5), (3, "b", 5), (3, "c", 5), (3, "c", 6)]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [(1, "a", 4), (2, "a", 4), (2, "b", 4), (2, "b", 5), (3, "b", 5), (3, "c", 5), (3, "c", 6)]) } diff --git a/Tests/AsyncAlgorithmsTests/TestCompacted.swift b/Tests/AsyncAlgorithmsTests/TestCompacted.swift index b82fe31e..3e2e5198 100644 --- a/Tests/AsyncAlgorithmsTests/TestCompacted.swift +++ b/Tests/AsyncAlgorithmsTests/TestCompacted.swift @@ -83,10 +83,10 @@ final class TestCompacted: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestInterspersed.swift b/Tests/AsyncAlgorithmsTests/TestInterspersed.swift deleted file mode 100644 index 79c93f73..00000000 --- a/Tests/AsyncAlgorithmsTests/TestInterspersed.swift +++ /dev/null @@ -1,91 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -import XCTest -import AsyncAlgorithms - -final class TestInterspersed: XCTestCase { - func test_interspersed() async { - let source = [1, 2, 3, 4, 5] - let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] - let sequence = source.async.interspersed(with: 0) - var actual = [Int]() - var iterator = sequence.makeAsyncIterator() - while let item = await iterator.next() { - actual.append(item) - } - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - XCTAssertEqual(actual, expected) - } - - func test_interspersed_empty() async { - let source = [Int]() - let expected = [Int]() - let sequence = source.async.interspersed(with: 0) - var actual = [Int]() - var iterator = sequence.makeAsyncIterator() - while let item = await iterator.next() { - actual.append(item) - } - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - XCTAssertEqual(actual, expected) - } - - func test_interspersed_with_throwing_upstream() async { - let source = [1, 2, 3, -1, 4, 5] - let expected = [1, 0, 2, 0, 3, 0] - var actual = [Int]() - let sequence = source.async.map { - try throwOn(-1, $0) - }.interspersed(with: 0) - - var iterator = sequence.makeAsyncIterator() - do { - while let item = try await iterator.next() { - actual.append(item) - } - XCTFail() - } catch { - XCTAssertEqual(Failure(), error as? Failure) - } - let pastEnd = try! await iterator.next() - XCTAssertNil(pastEnd) - XCTAssertEqual(actual, expected) - } - - func test_cancellation() async { - let source = Indefinite(value: "test") - let sequence = source.async.interspersed(with: "sep") - let finished = expectation(description: "finished") - let iterated = expectation(description: "iterated") - let task = Task { - - var iterator = sequence.makeAsyncIterator() - let _ = await iterator.next() - iterated.fulfill() - - while let _ = await iterator.next() { } - - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - - finished.fulfill() - } - // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) - // cancellation should ensure the loop finishes - // without regards to the remaining underlying sequence - task.cancel() - wait(for: [finished], timeout: 1.0) - } -} diff --git a/Tests/AsyncAlgorithmsTests/TestJoin.swift b/Tests/AsyncAlgorithmsTests/TestJoin.swift index 91de1ef9..c60b71da 100644 --- a/Tests/AsyncAlgorithmsTests/TestJoin.swift +++ b/Tests/AsyncAlgorithmsTests/TestJoin.swift @@ -122,11 +122,11 @@ final class TestJoinedBySeparator: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } @@ -206,10 +206,10 @@ final class TestJoined: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestLazy.swift b/Tests/AsyncAlgorithmsTests/TestLazy.swift index 1ef69d29..3ff8c5c1 100644 --- a/Tests/AsyncAlgorithmsTests/TestLazy.swift +++ b/Tests/AsyncAlgorithmsTests/TestLazy.swift @@ -121,12 +121,12 @@ final class TestLazy: XCTestCase { } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestManualClock.swift b/Tests/AsyncAlgorithmsTests/TestManualClock.swift index 045ba2de..15cc97bb 100644 --- a/Tests/AsyncAlgorithmsTests/TestManualClock.swift +++ b/Tests/AsyncAlgorithmsTests/TestManualClock.swift @@ -29,7 +29,7 @@ final class TestManualClock: XCTestCase { clock.advance() XCTAssertFalse(state.withCriticalRegion { $0 }) clock.advance() - wait(for: [afterSleep], timeout: 1.0) + await fulfillment(of: [afterSleep], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) } @@ -51,7 +51,7 @@ final class TestManualClock: XCTestCase { XCTAssertFalse(state.withCriticalRegion { $0 }) clock.advance() task.cancel() - wait(for: [afterSleep], timeout: 1.0) + await fulfillment(of: [afterSleep], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) XCTAssertTrue(failure.withCriticalRegion { $0 is CancellationError }) } @@ -73,7 +73,7 @@ final class TestManualClock: XCTestCase { } XCTAssertFalse(state.withCriticalRegion { $0 }) task.cancel() - wait(for: [afterSleep], timeout: 1.0) + await fulfillment(of: [afterSleep], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) XCTAssertTrue(failure.withCriticalRegion { $0 is CancellationError }) } diff --git a/Tests/AsyncAlgorithmsTests/TestMerge.swift b/Tests/AsyncAlgorithmsTests/TestMerge.swift index 0bcb3479..c8d5e1ce 100644 --- a/Tests/AsyncAlgorithmsTests/TestMerge.swift +++ b/Tests/AsyncAlgorithmsTests/TestMerge.swift @@ -185,11 +185,11 @@ final class TestMerge2: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_merge_when_cancelled() async { @@ -509,11 +509,11 @@ final class TestMerge3: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } // MARK: - IteratorInitialized diff --git a/Tests/AsyncAlgorithmsTests/TestReductions.swift b/Tests/AsyncAlgorithmsTests/TestReductions.swift index ad9cf5ac..24e7e4e4 100644 --- a/Tests/AsyncAlgorithmsTests/TestReductions.swift +++ b/Tests/AsyncAlgorithmsTests/TestReductions.swift @@ -217,10 +217,10 @@ final class TestReductions: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift b/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift index 01ddf3ff..932585b0 100644 --- a/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift +++ b/Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift @@ -95,10 +95,10 @@ final class TestRemoveDuplicates: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/Tests/AsyncAlgorithmsTests/TestThrottle.swift b/Tests/AsyncAlgorithmsTests/TestThrottle.swift index 72c90f65..d7b60db3 100644 --- a/Tests/AsyncAlgorithmsTests/TestThrottle.swift +++ b/Tests/AsyncAlgorithmsTests/TestThrottle.swift @@ -17,7 +17,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(0), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(0), clock: $0.clock) "abcdefghijk|" } } @@ -26,7 +26,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(0), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(0), clock: $0.clock, latest: false) "abcdefghijk|" } } @@ -35,7 +35,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(1), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(1), clock: $0.clock) "abcdefghijk|" } } @@ -44,7 +44,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(1), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(1), clock: $0.clock, latest: false) "abcdefghijk|" } } @@ -53,7 +53,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock) "a-c-e-g-i-k|" } } @@ -62,7 +62,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock, latest: false) "a-b-d-f-h-j|" } } @@ -71,8 +71,8 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(3), clock: $0.clock) - "a--d--g--j-|" + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock) + "a--d--g--j--[k|]" } } @@ -80,8 +80,8 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdefghijk|" - $0.inputs[0].throttle(for: .steps(3), clock: $0.clock, latest: false) - "a--b--e--h-|" + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock, latest: false) + "a--b--e--h--[k|]" } } @@ -89,7 +89,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdef^hijk|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock) "a-c-e-^" } } @@ -98,7 +98,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "abcdef^hijk|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock, latest: false) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock, latest: false) "a-b-d-^" } } @@ -107,7 +107,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "-a-b-c-d-e-f-g-h-i-j-k-|" - $0.inputs[0].throttle(for: .steps(1), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(1), clock: $0.clock) "-a-b-c-d-e-f-g-h-i-j-k-|" } } @@ -116,7 +116,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "-a-b-c-d-e-f-g-h-i-j-k-|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock) "-a-b-c-d-e-f-g-h-i-j-k-|" } } @@ -125,7 +125,7 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "--a--b--c--d--e--f--g|" - $0.inputs[0].throttle(for: .steps(2), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(2), clock: $0.clock) "--a--b--c--d--e--f--g|" } } @@ -134,8 +134,26 @@ final class TestThrottle: XCTestCase { guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } validate { "-a-b-c-d-e-f-g-h-i-j-k-|" - $0.inputs[0].throttle(for: .steps(3), clock: $0.clock) + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock) "-a---c---e---g---i---k-|" } } + + func test_trailing_delay_without_latest() throws { + guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } + validate { + "abcdefghijkl|" + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock, latest: false) + "a--b--e--h--[k|]" + } + } + + func test_trailing_delay_with_latest() throws { + guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { throw XCTSkip("Skipped due to Clock/Instant/Duration availability") } + validate { + "abcdefghijkl|" + $0.inputs[0]._throttle(for: .steps(3), clock: $0.clock, latest: true) + "a--d--g--j--[l|]" + } + } } diff --git a/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift b/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift index 7dd60f18..6110c884 100644 --- a/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift +++ b/Tests/AsyncAlgorithmsTests/TestThrowingChannel.swift @@ -270,7 +270,7 @@ final class TestThrowingChannel: XCTestCase { task1.cancel() // Then: the first sending operation is resumed - wait(for: [send1IsResumed], timeout: 1.0) + await fulfillment(of: [send1IsResumed], timeout: 1.0) // When: collecting elements var iterator = sut.makeAsyncIterator() diff --git a/Tests/AsyncAlgorithmsTests/TestValidator.swift b/Tests/AsyncAlgorithmsTests/TestValidator.swift index 9c5ef9c2..67d2f37e 100644 --- a/Tests/AsyncAlgorithmsTests/TestValidator.swift +++ b/Tests/AsyncAlgorithmsTests/TestValidator.swift @@ -24,7 +24,7 @@ final class TestValidator: XCTestCase { } XCTAssertFalse(state.withCriticalRegion { $0 }) gate.open() - wait(for: [entered], timeout: 1.0) + await fulfillment(of: [entered], timeout: 1.0) XCTAssertTrue(state.withCriticalRegion { $0 }) } @@ -52,18 +52,18 @@ final class TestValidator: XCTestCase { } finished.fulfill() } - wait(for: [started], timeout: 1.0) + await fulfillment(of: [started], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, []) gated.advance() - wait(for: [expectations[0]], timeout: 1.0) + await fulfillment(of: [expectations[0]], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1]) gated.advance() - wait(for: [expectations[1]], timeout: 1.0) + await fulfillment(of: [expectations[1]], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1, 2]) gated.advance() - wait(for: [expectations[2]], timeout: 1.0) + await fulfillment(of: [expectations[2]], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1, 2, 3]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_gatedSequence_throwing() async { @@ -93,14 +93,14 @@ final class TestValidator: XCTestCase { } finished.fulfill() } - wait(for: [started], timeout: 1.0) + await fulfillment(of: [started], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, []) gated.advance() - wait(for: [expectations[0]], timeout: 1.0) + await fulfillment(of: [expectations[0]], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1]) gated.advance() XCTAssertEqual(state.withCriticalRegion { $0 }, [1]) - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) XCTAssertEqual(state.withCriticalRegion { $0 }, [1]) XCTAssertEqual(failure.withCriticalRegion { $0 as? Failure }, Failure()) } @@ -131,7 +131,7 @@ final class TestValidator: XCTestCase { XCTAssertEqual(value, [2, 3, 4]) a.advance() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) value = validator.current XCTAssertEqual(value, [2, 3, 4]) } diff --git a/Tests/AsyncAlgorithmsTests/TestZip.swift b/Tests/AsyncAlgorithmsTests/TestZip.swift index 25bc9ec7..4c2fb229 100644 --- a/Tests/AsyncAlgorithmsTests/TestZip.swift +++ b/Tests/AsyncAlgorithmsTests/TestZip.swift @@ -152,11 +152,11 @@ final class TestZip2: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } func test_zip_when_cancelled() async { @@ -370,10 +370,10 @@ final class TestZip3: XCTestCase { finished.fulfill() } // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) + await fulfillment(of: [iterated], timeout: 1.0) // cancellation should ensure the loop finishes // without regards to the remaining underlying sequence task.cancel() - wait(for: [finished], timeout: 1.0) + await fulfillment(of: [finished], timeout: 1.0) } } diff --git a/bin/update-gh-pages-documentation-site b/bin/update-gh-pages-documentation-site deleted file mode 100755 index 0c1e59aa..00000000 --- a/bin/update-gh-pages-documentation-site +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash -# -# This source file is part of the Swift.org 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 -# See https://swift.org/CONTRIBUTORS.txt for Swift project authors -# -# Updates the GitHub Pages documentation site thats published from the 'docs' -# subdirectory in the 'gh-pages' branch of this repository. -# -# This script should be run by someone with commit access to the 'gh-pages' branch -# at a regular frequency so that the documentation content on the GitHub Pages site -# is up-to-date with the content in this repo. -# - -export BUILDING_FOR_DOCUMENTATION_GENERATION=1 - -set -eu - -# A `realpath` alternative using the default C implementation. -filepath() { - [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" -} - -SWIFT_ASYNC_ALGORITHMS_ROOT="$(dirname $(dirname $(filepath $0)))" - -ASYNC_ALGORITHMS_BUILD_DIR="$SWIFT_ASYNC_ALGORITHMS_ROOT"/.build/async-algorithms-gh-pages-build - -# Set current directory to the repository root -cd "$SWIFT_ASYNC_ALGORITHMS_ROOT" - -# Use git worktree to checkout the gh-pages branch of this repository in a gh-pages sub-directory -git fetch -git worktree add --checkout gh-pages origin/gh-pages - -# Pretty print DocC JSON output so that it can be consistently diffed between commits -export DOCC_JSON_PRETTYPRINT="YES" - -# Generate documentation for the 'AsyncAlgorithms' target and output it -# to the /docs subdirectory in the gh-pages worktree directory. -swift package \ - --allow-writing-to-directory "$SWIFT_ASYNC_ALGORITHMS_ROOT/gh-pages/docs" \ - generate-documentation \ - --target AsyncAlgorithms \ - --disable-indexing \ - --transform-for-static-hosting \ - --hosting-base-path swift-async-algorithms \ - --output-path "$SWIFT_ASYNC_ALGORITHMS_ROOT/gh-pages/docs" - -# Save the current commit we've just built documentation from in a variable -CURRENT_COMMIT_HASH=`git rev-parse --short HEAD` - -# Commit and push our changes to the gh-pages branch -cd gh-pages -git add docs - -if [ -n "$(git status --porcelain)" ]; then - echo "Documentation changes found. Commiting the changes to the 'gh-pages' branch and pushing to origin." - git commit -m "Update GitHub Pages documentation site to $CURRENT_COMMIT_HASH" - git push origin HEAD:gh-pages -else - # No changes found, nothing to commit. - echo "No documentation changes found." -fi - -# Delete the git worktree we created -cd .. -git worktree remove gh-pages diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..e592f92f --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,24 @@ +ARG swift_version=5.7 +ARG ubuntu_version=focal +ARG base_image=swift:$swift_version-$ubuntu_version +FROM $base_image +# needed to do again after FROM due to docker limitation +ARG swift_version +ARG ubuntu_version + +# set as UTF-8 +RUN apt-get update && apt-get install -y locales locales-all +ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US.UTF-8 + +# tools +RUN mkdir -p $HOME/.tools +RUN echo 'export PATH="$HOME/.tools:$PATH"' >> $HOME/.profile + +# swiftformat (until part of the toolchain) + +ARG swiftformat_version=0.51.12 +RUN git clone --branch $swiftformat_version --depth 1 https://github.com/nicklockwood/SwiftFormat $HOME/.tools/swift-format +RUN cd $HOME/.tools/swift-format && swift build -c release +RUN ln -s $HOME/.tools/swift-format/.build/release/swiftformat $HOME/.tools/swiftformat diff --git a/docker/docker-compose.2004.57.yaml b/docker/docker-compose.2004.57.yaml new file mode 100644 index 00000000..19c52d83 --- /dev/null +++ b/docker/docker-compose.2004.57.yaml @@ -0,0 +1,22 @@ +version: "3" + +services: + + runtime-setup: + image: swift-async-algorithms:20.04-5.7 + build: + args: + ubuntu_version: "focal" + swift_version: "5.7" + + build: + image: swift-async-algorithms:20.04-5.7 + + test: + image: swift-async-algorithms:20.04-5.7 + environment: [] + #- SANITIZER_ARG: "--sanitize=thread" + #- TSAN_OPTIONS: "no_huge_pages_for_shadow=0 suppressions=/code/tsan_suppressions.txt" + + shell: + image: swift-async-algorithms:20.04-5.7 diff --git a/docker/docker-compose.2004.58.yaml b/docker/docker-compose.2004.58.yaml new file mode 100644 index 00000000..56d83dfc --- /dev/null +++ b/docker/docker-compose.2004.58.yaml @@ -0,0 +1,21 @@ +version: "3" + +services: + + runtime-setup: + image: swift-async-algorithms:20.04-5.8 + build: + args: + base_image: "swiftlang/swift:nightly-5.8-focal" + + build: + image: swift-async-algorithms:20.04-5.8 + + test: + image: swift-async-algorithms:20.04-5.8 + environment: [] + #- SANITIZER_ARG: "--sanitize=thread" + #- TSAN_OPTIONS: "no_huge_pages_for_shadow=0 suppressions=/code/tsan_suppressions.txt" + + shell: + image: swift-async-algorithms:20.04-5.8 diff --git a/docker/docker-compose.2204.main.yaml b/docker/docker-compose.2204.main.yaml new file mode 100644 index 00000000..f28e21d2 --- /dev/null +++ b/docker/docker-compose.2204.main.yaml @@ -0,0 +1,21 @@ +version: "3" + +services: + + runtime-setup: + image: swift-async-algorithms:22.04-main + build: + args: + base_image: "swiftlang/swift:nightly-main-jammy" + + build: + image: swift-async-algorithms:22.04-main + + test: + image: swift-async-algorithms:22.04-main + environment: [] + #- SANITIZER_ARG: "--sanitize=thread" + #- TSAN_OPTIONS: "no_huge_pages_for_shadow=0 suppressions=/code/tsan_suppressions.txt" + + shell: + image: swift-async-algorithms:22.04-main diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 00000000..8d1d9a33 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,39 @@ +# this file is not designed to be run directly +# instead, use the docker-compose.. files +# eg docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.2004.56.yaml run test +version: "3" + +services: + runtime-setup: + image: swift-async-algorithms:default + build: + context: . + dockerfile: Dockerfile + + common: &common + image: swift-async-algorithms:default + depends_on: [runtime-setup] + volumes: + - ~/.ssh:/root/.ssh + - ..:/code:z + working_dir: /code + + soundness: + <<: *common + command: /bin/bash -xcl "swift -version && uname -a && ./scripts/soundness.sh" + + build: + <<: *common + environment: [] + command: /bin/bash -cl "swift build" + + test: + <<: *common + depends_on: [runtime-setup] + command: /bin/bash -xcl "swift $${SWIFT_TEST_VERB-test} $${WARN_AS_ERROR_ARG-} $${SANITIZER_ARG-} $${IMPORT_CHECK_ARG-}" + + # util + + shell: + <<: *common + entrypoint: /bin/bash