├── .github └── workflows │ └── pull_request.yml ├── .gitignore ├── .license_header_template ├── .licenseignore ├── .spi.yml ├── .swift-format ├── .yamllint.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Evolution ├── 0000-swift-async-algorithms-template.md ├── 0001-zip.md ├── 0002-merge.md ├── 0003-compacted.md ├── 0004-joined.md ├── 0005-adjacent-pairs.md ├── 0006-combineLatest.md ├── 0007-chain.md ├── 0008-bytes.md ├── 0009-async.md ├── 0010-buffer.md ├── 0011-interspersed.md ├── NNNN-channel.md ├── NNNN-chunk.md ├── NNNN-rate-limits.md └── NNNN-reductions.md ├── LICENSE.txt ├── Package.swift ├── Package@swift-5.7.swift ├── README.md ├── Sources ├── AsyncAlgorithms │ ├── AsyncAdjacentPairsSequence.swift │ ├── AsyncAlgorithms.docc │ │ ├── AsyncAlgorithms.md │ │ └── Guides │ │ │ ├── AdjacentPairs.md │ │ │ ├── BufferedBytes.md │ │ │ ├── Chain.md │ │ │ ├── Channel.md │ │ │ ├── Chunked.md │ │ │ ├── Collections.md │ │ │ ├── CombineLatest.md │ │ │ ├── Compacted.md │ │ │ ├── Debounce.md │ │ │ ├── Effects.md │ │ │ ├── Intersperse.md │ │ │ ├── Joined.md │ │ │ ├── Lazy.md │ │ │ ├── Merge.md │ │ │ ├── Reductions.md │ │ │ ├── RemoveDuplicates.md │ │ │ ├── Throttle.md │ │ │ ├── Timer.md │ │ │ └── Zip.md │ ├── AsyncBufferedByteIterator.swift │ ├── AsyncChain2Sequence.swift │ ├── AsyncChain3Sequence.swift │ ├── AsyncChunkedByGroupSequence.swift │ ├── AsyncChunkedOnProjectionSequence.swift │ ├── AsyncChunksOfCountOrSignalSequence.swift │ ├── AsyncChunksOfCountSequence.swift │ ├── AsyncCompactedSequence.swift │ ├── AsyncExclusiveReductionsSequence.swift │ ├── AsyncInclusiveReductionsSequence.swift │ ├── AsyncJoinedBySeparatorSequence.swift │ ├── AsyncJoinedSequence.swift │ ├── AsyncRemoveDuplicatesSequence.swift │ ├── AsyncSyncSequence.swift │ ├── AsyncThrottleSequence.swift │ ├── AsyncThrowingExclusiveReductionsSequence.swift │ ├── AsyncThrowingInclusiveReductionsSequence.swift │ ├── AsyncTimerSequence.swift │ ├── Buffer │ │ ├── AsyncBufferSequence.swift │ │ ├── BoundedBufferStateMachine.swift │ │ ├── BoundedBufferStorage.swift │ │ ├── UnboundedBufferStateMachine.swift │ │ └── UnboundedBufferStorage.swift │ ├── Channels │ │ ├── AsyncChannel.swift │ │ ├── AsyncThrowingChannel.swift │ │ ├── ChannelStateMachine.swift │ │ └── ChannelStorage.swift │ ├── CombineLatest │ │ ├── AsyncCombineLatest2Sequence.swift │ │ ├── AsyncCombineLatest3Sequence.swift │ │ ├── CombineLatestStateMachine.swift │ │ └── CombineLatestStorage.swift │ ├── Debounce │ │ ├── AsyncDebounceSequence.swift │ │ ├── DebounceStateMachine.swift │ │ └── DebounceStorage.swift │ ├── Dictionary.swift │ ├── Interspersed │ │ └── AsyncInterspersedSequence.swift │ ├── Locking.swift │ ├── Merge │ │ ├── AsyncMerge2Sequence.swift │ │ ├── AsyncMerge3Sequence.swift │ │ ├── MergeStateMachine.swift │ │ └── MergeStorage.swift │ ├── RangeReplaceableCollection.swift │ ├── Rethrow.swift │ ├── SetAlgebra.swift │ ├── UnsafeTransfer.swift │ └── Zip │ │ ├── AsyncZip2Sequence.swift │ │ ├── AsyncZip3Sequence.swift │ │ ├── ZipStateMachine.swift │ │ └── ZipStorage.swift ├── AsyncAlgorithms_XCTest │ └── ValidationTest.swift ├── AsyncSequenceValidation │ ├── AsyncSequenceValidation.docc │ │ ├── AsyncSequenceValidation.md │ │ └── Validation.md │ ├── AsyncSequenceValidationDiagram.swift │ ├── Clock.swift │ ├── Event.swift │ ├── Expectation.swift │ ├── Input.swift │ ├── Job.swift │ ├── Locking.swift │ ├── SourceLocation.swift │ ├── TaskDriver.swift │ ├── Test.swift │ ├── Theme.swift │ └── WorkQueue.swift └── _CAsyncSequenceValidationSupport │ ├── _CAsyncSequenceValidationSupport.h │ └── module.modulemap └── Tests └── AsyncAlgorithmsTests ├── Interspersed └── TestInterspersed.swift ├── Performance ├── TestThroughput.swift └── ThroughputMeasurement.swift ├── Support ├── Asserts.swift ├── Failure.swift ├── Gate.swift ├── GatedSequence.swift ├── Indefinite.swift ├── Locking.swift ├── ManualClock.swift ├── ReportingSequence.swift ├── Validator.swift └── ViolatingSequence.swift ├── TestAdjacentPairs.swift ├── TestBuffer.swift ├── TestBufferedByteIterator.swift ├── TestChain.swift ├── TestChannel.swift ├── TestChunk.swift ├── TestCombineLatest.swift ├── TestCompacted.swift ├── TestDebounce.swift ├── TestDictionary.swift ├── TestJoin.swift ├── TestLazy.swift ├── TestManualClock.swift ├── TestMerge.swift ├── TestRangeReplaceableCollection.swift ├── TestReductions.swift ├── TestRemoveDuplicates.swift ├── TestSetAlgebra.swift ├── TestThrottle.swift ├── TestThrowingChannel.swift ├── TestTimer.swift ├── TestValidationTests.swift ├── TestValidator.swift └── TestZip.swift /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull request 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | 7 | jobs: 8 | tests: 9 | name: Test 10 | uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main 11 | soundness: 12 | name: Soundness 13 | uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main 14 | with: 15 | license_header_check_project_name: "Swift Async Algorithms" 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | .swiftpm 11 | Package.resolved -------------------------------------------------------------------------------- /.license_header_template: -------------------------------------------------------------------------------- 1 | @@===----------------------------------------------------------------------===@@ 2 | @@ 3 | @@ This source file is part of the Swift Async Algorithms open source project 4 | @@ 5 | @@ Copyright (c) YEARS Apple Inc. and the Swift project authors 6 | @@ Licensed under Apache License v2.0 with Runtime Library Exception 7 | @@ 8 | @@ See https://swift.org/LICENSE.txt for license information 9 | @@ 10 | @@===----------------------------------------------------------------------===@@ 11 | -------------------------------------------------------------------------------- /.licenseignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | **/.gitignore 3 | .licenseignore 4 | .gitattributes 5 | .mailfilter 6 | .mailmap 7 | .spi.yml 8 | .swift-format 9 | .editorconfig 10 | .github/* 11 | *.md 12 | *.txt 13 | *.yml 14 | *.yaml 15 | *.json 16 | Package.swift 17 | **/Package.swift 18 | Package@-*.swift 19 | Package@swift-*.swift 20 | **/Package@-*.swift 21 | Package.resolved 22 | **/Package.resolved 23 | Makefile 24 | *.modulemap 25 | **/*.modulemap 26 | **/*.docc/* 27 | *.xcprivacy 28 | **/*.xcprivacy 29 | *.symlink 30 | **/*.symlink 31 | Dockerfile 32 | **/Dockerfile 33 | Snippets/* 34 | dev/git.commit.template 35 | *.crt 36 | **/*.crt 37 | *.pem 38 | **/*.pem 39 | *.der 40 | **/*.der 41 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [AsyncAlgorithms] 5 | swift_version: 5.8 6 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy" : { 3 | "accessLevel" : "private" 4 | }, 5 | "indentation" : { 6 | "spaces" : 2 7 | }, 8 | "indentConditionalCompilationBlocks" : false, 9 | "indentSwitchCaseLabels" : false, 10 | "lineBreakAroundMultilineExpressionChainComponents" : false, 11 | "lineBreakBeforeControlFlowKeywords" : false, 12 | "lineBreakBeforeEachArgument" : true, 13 | "lineBreakBeforeEachGenericRequirement" : true, 14 | "lineLength" : 120, 15 | "maximumBlankLines" : 1, 16 | "prioritizeKeepingFunctionOutputTogether" : true, 17 | "respectsExistingLineBreaks" : true, 18 | "rules" : { 19 | "AllPublicDeclarationsHaveDocumentation" : false, 20 | "AlwaysUseLowerCamelCase" : false, 21 | "AmbiguousTrailingClosureOverload" : true, 22 | "BeginDocumentationCommentWithOneLineSummary" : false, 23 | "DoNotUseSemicolons" : true, 24 | "DontRepeatTypeInStaticProperties" : true, 25 | "FileScopedDeclarationPrivacy" : true, 26 | "FullyIndirectEnum" : true, 27 | "GroupNumericLiterals" : true, 28 | "IdentifiersMustBeASCII" : true, 29 | "NeverForceUnwrap" : false, 30 | "NeverUseForceTry" : false, 31 | "NeverUseImplicitlyUnwrappedOptionals" : false, 32 | "NoAccessLevelOnExtensionDeclaration" : true, 33 | "NoAssignmentInExpressions" : true, 34 | "NoBlockComments" : true, 35 | "NoCasesWithOnlyFallthrough" : true, 36 | "NoEmptyTrailingClosureParentheses" : true, 37 | "NoLabelsInCasePatterns" : false, 38 | "NoLeadingUnderscores" : false, 39 | "NoParensAroundConditions" : true, 40 | "NoVoidReturnOnFunctionSignature" : true, 41 | "OneCasePerLine" : true, 42 | "OneVariableDeclarationPerLine" : true, 43 | "OnlyOneTrailingClosureArgument" : true, 44 | "OrderedImports" : false, 45 | "ReturnVoidInsteadOfEmptyTuple" : true, 46 | "UseEarlyExits" : true, 47 | "UseLetInEveryBoundCaseVariable" : false, 48 | "UseShorthandTypeNames" : true, 49 | "UseSingleLinePropertyGetter" : false, 50 | "UseSynthesizedInitializer" : false, 51 | "UseTripleSlashForDocumentationComments" : true, 52 | "UseWhereClausesInForLoops" : false, 53 | "ValidateDocumentationComments" : false 54 | }, 55 | "spacesAroundRangeFormationOperators" : false, 56 | "tabWidth" : 4, 57 | "version" : 1 58 | } -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | extends: default 2 | 3 | rules: 4 | line-length: false 5 | document-start: false 6 | truthy: 7 | check-keys: false # Otherwise we get a false positive on GitHub action's `on` key 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The code of conduct for this project can be found at https://swift.org/code-of-conduct. 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | By submitting a pull request, you represent that you have the right to license 2 | your contribution to Apple and the community, and agree by submitting the patch 3 | that your contributions are licensed under the [Swift 4 | license](https://swift.org/LICENSE.txt). 5 | 6 | --- 7 | 8 | Before submitting the pull request, please make sure you have tested your 9 | changes and that they follow the Swift project [guidelines for contributing 10 | code](https://swift.org/contributing/#contributing-code). 11 | -------------------------------------------------------------------------------- /Evolution/0000-swift-async-algorithms-template.md: -------------------------------------------------------------------------------- 1 | # Feature name 2 | 3 | * Proposal: [NNNN](NNNN-filename.md) 4 | * Authors: [Author 1](https://github.com/swiftdev), [Author 2](https://github.com/swiftdev) 5 | * Review Manager: TBD 6 | * Status: **Awaiting implementation** 7 | 8 | *During the review process, add the following fields as needed:* 9 | 10 | * Implementation: [apple/swift-async-algorithms#NNNNN](https://github.com/apple/swift-async-algorithms/pull/NNNNN) 11 | * Decision Notes: [Rationale](https://forums.swift.org/), [Additional Commentary](https://forums.swift.org/) 12 | * Bugs: [NNNN](https://github.com/apple/swift-async-algorithms/issues) 13 | 14 | ## Introduction 15 | 16 | A short description of what the feature is. Try to keep it to a 17 | single-paragraph "elevator pitch" so the reader understands what 18 | problem this proposal is addressing. 19 | 20 | Swift forums thread: [Discussion thread topic for that proposal](https://forums.swift.org/) 21 | 22 | ## Motivation 23 | 24 | Describe the problems that this proposal seeks to address. If the 25 | problem is that some common pattern is currently hard to express, show 26 | how one can currently get a similar effect and describe its 27 | drawbacks. If it's completely new functionality that cannot be 28 | emulated, motivate why this new functionality would help Swift 29 | developers create better Swift code. 30 | 31 | ## Proposed solution 32 | 33 | Describe your solution to the problem. Provide examples and describe 34 | how they work. Show how your solution is better than current 35 | workarounds: is it cleaner, safer, or more efficient? 36 | 37 | ## Detailed design 38 | 39 | Describe the design of the solution in detail. If it involves new 40 | syntax in the language, show the additions and changes to the Swift 41 | grammar. If it's a new API, show the full API and its documentation 42 | comments detailing what it does. The detail in this section should be 43 | sufficient for someone who is *not* one of the authors to be able to 44 | reasonably implement the feature. 45 | 46 | ## Effect on API resilience 47 | 48 | API resilience describes the changes one can make to a public API 49 | without breaking its ABI. Does this proposal introduce features that 50 | would become part of a public API? If so, what kinds of changes can be 51 | made without breaking ABI? Can this feature be added/removed without 52 | breaking ABI? For more information about the resilience model, see the 53 | [library evolution 54 | document](https://github.com/apple/swift/blob/main/docs/LibraryEvolution.rst) 55 | in the Swift repository. 56 | 57 | ## Alternatives considered 58 | 59 | Describe alternative approaches to addressing the same problem, and 60 | why you chose this approach instead. 61 | 62 | ## Acknowledgments 63 | 64 | If significant changes or improvements suggested by members of the 65 | community were incorporated into the proposal as it developed, take a 66 | moment here to thank them for their contributions. Swift evolution is a 67 | collaborative process, and everyone's input should receive recognition! 68 | -------------------------------------------------------------------------------- /Evolution/0003-compacted.md: -------------------------------------------------------------------------------- 1 | # Compacted 2 | 3 | * Proposal: [SAA-0003](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0003-compacted.md) 4 | * Authors: [Philippe Hausler](https://github.com/phausler) 5 | * Status: **Accepted** 6 | 7 | * Implementation: [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift) 8 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestCompacted.swift) 9 | 10 | ## Proposed Solution 11 | 12 | Similar to the Swift Algorithms package we propose that a new method be added to `AsyncSequence` to fit this need. 13 | 14 | ```swift 15 | extension AsyncSequence { 16 | public func compacted() -> AsyncCompactedSequence 17 | where Element == Unwrapped? 18 | } 19 | ``` 20 | 21 | This is equivalent to writing `.compactMap { $0 }` from a behavioral standpoint but is easier to reason about and is more efficient since it does not need to execute or store a closure. 22 | 23 | ## Detailed Design 24 | 25 | The `AsyncCompactedSequence` type from an effects standpoint works just like `AsyncCompactMapSequence`. When the base asynchronous sequence throws, the iteration of `AsyncCompactedSequence` can throw. Likewise if the base does not throw then the iteration of `AsyncCompactedSequence` does not throw. This type is conditionally `Sendable` when the base, base element, and base iterator are `Sendable. 26 | 27 | ```swift 28 | public struct AsyncCompactedSequence: AsyncSequence 29 | where Base.Element == Element? { 30 | 31 | public struct Iterator: AsyncIteratorProtocol { 32 | public mutating func next() async rethrows -> Element? 33 | } 34 | 35 | public func makeAsyncIterator() -> Iterator { 36 | Iterator(base.makeAsyncIterator()) 37 | } 38 | } 39 | 40 | extension AsyncCompactedSequence: Sendable 41 | where 42 | Base: Sendable, Base.Element: Sendable, 43 | Base.AsyncIterator: Sendable { } 44 | 45 | extension AsyncCompactedSequence.Iterator: Sendable 46 | where 47 | Base: Sendable, Base.Element: Sendable, 48 | Base.AsyncIterator: Sendable { } 49 | ``` 50 | 51 | ## Effect on API resilience 52 | 53 | Compacted has a trivial implementation and is marked as `@frozen` and `@inlinable`. This removes the ability of this type and functions to be ABI resilient boundaries at the benefit of being highly optimizable. 54 | 55 | ## Alternatives considered 56 | 57 | None; shy of potentially eliding this since the functionality is so trivial. However the utility of this function aides in ease of use and approachability along with parity with the Swift Algorithms package. 58 | 59 | ## Acknowledgments 60 | 61 | This transformation function is a direct analog to the synchronous version [defined in the Swift Algorithms package](https://github.com/apple/swift-algorithms/blob/main/Guides/Compacted.md) 62 | -------------------------------------------------------------------------------- /Evolution/0004-joined.md: -------------------------------------------------------------------------------- 1 | # Joined 2 | 3 | * Proposal: [SAA-0004](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0004-joined.md) 4 | * Authors: [Philippe Hausler](https://github.com/phausler) 5 | * Review Manager: [Franz Busch](https://github.com/FranzBusch) 6 | * Status: **Accepted** 7 | 8 | * Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncJoinedSequence.swift) | 9 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestJoin.swift)] 10 | * Decision Notes: 11 | * Bugs: 12 | 13 | ## Introduction 14 | 15 | The `joined()` and `joined(separator:)` algorithms on `AsyncSequence`s provide APIs to concatenate an `AsyncSequence` of `AsyncSequence`s. 16 | 17 | ```swift 18 | extension AsyncSequence where Element: AsyncSequence { 19 | public func joined() -> AsyncJoinedSequence 20 | } 21 | 22 | extension AsyncSequence where Element: AsyncSequence { 23 | public func joined(separator: Separator) -> AsyncJoinedBySeparatorSequence 24 | } 25 | ``` 26 | 27 | ## Detailed Design 28 | 29 | These algorithms iterate over the elements of each `AsyncSequence` one bye one, i.e. only after the iteration of one `AsyncSequence` has finished the next one will be started. 30 | 31 | ```swift 32 | let appleFeed = URL("http://www.example.com/ticker?symbol=AAPL").lines 33 | let nasdaqFeed = URL("http://www.example.com/ticker?symbol=^IXIC").lines 34 | 35 | for try await line in [appleFeed, nasdaqFeed].async.joined() { 36 | print("\(line)") 37 | } 38 | ``` 39 | 40 | Given some sample inputs the following combined events can be expected. 41 | 42 | | Timestamp | appleFeed | nasdaqFeed | output | 43 | | ----------- | --------- | ---------- | ----------------------------- | 44 | | 11:40 AM | 173.91 | | 173.91 | 45 | | 12:25 AM | | 14236.78 | | 46 | | 12:40 AM | | 14218.34 | | 47 | | 1:15 PM | 173.00 | | 173.00 | 48 | | 1:15 PM | | | 14236.78 | 49 | | 1:15 PM | | | 14218.34 | 50 | 51 | 52 | The `joined()` and `joined(separator:)` methods are available on `AsyncSequence`s with elements that are `AsyncSequence`s themselves and produce either an `AsyncJoinedSequence` or an `AsyncJoinedBySeparatorSequence`. 53 | 54 | As soon as an inner `AsyncSequence` returns `nil` the algorithm continues with iterating the next inner `AsyncSequence`. 55 | 56 | The throwing behaviour of `AsyncJoinedSequence` and `AsyncJoinedBySeparatorSequence` is that if any of the inner `AsyncSequence`s throws, then the composed sequence throws on its iteration. 57 | 58 | ### Naming 59 | 60 | The naming follows to current method naming of the standard library's [`joined`](https://developer.apple.com/documentation/swift/array/joined(separator:)-7uber) method. 61 | Prior art in the reactive community often names this method `concat`; however, we think that an alignment with the current method on `Sequence` is better. 62 | 63 | ### Comparison with other libraries 64 | 65 | **ReactiveX** ReactiveX has an [API definition of Concat](https://reactivex.io/documentation/operators/concat.html) as a top level function for concatenating Observables. 66 | 67 | **Combine** Combine has an [API definition of append](https://developer.apple.com/documentation/combine/publisher/append(_:)-5yh02) which offers similar functionality but limited to concatenating two individual `Publisher`s. 68 | -------------------------------------------------------------------------------- /Evolution/0005-adjacent-pairs.md: -------------------------------------------------------------------------------- 1 | # AdjacentPairs 2 | 3 | * Proposal: [SAA-0005](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0005-adjacent-pairs.md) 4 | * Author(s): [László Teveli](https://github.com/tevelee) 5 | * Review Manager: [Philippe Hausler](https://github.com/phausler) 6 | * Status: **Accepted** 7 | * Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift) | 8 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift)] 9 | * Decision Notes: 10 | * Bugs: 11 | 12 | ## Introduction 13 | 14 | The `adjacentPairs()` API serve the purpose of collecting adjacent values. This operation is available for any `AsyncSequence` by calling the `adjacentPairs()` method. 15 | 16 | ```swift 17 | extension AsyncSequence { 18 | public func adjacentPairs() -> AsyncAdjacentPairsSequence 19 | } 20 | ``` 21 | 22 | ## Detailed Design 23 | 24 | The `adjacentPairs()` algorithm produces elements of tuple (size of 2), containing a pair of the original `Element` type. 25 | 26 | The interface for this algorithm is available on all `AsyncSequence` types. The returned `AsyncAdjacentPairsSequence` conditionally conforms to `Sendable`. 27 | 28 | Its iterator keeps track of the previous element returned in the `next()` function and updates it in every turn. 29 | 30 | ```swift 31 | for await (first, second) in (1...5).async.adjacentPairs() { 32 | print("First: \(first), Second: \(second)") 33 | } 34 | 35 | // First: 1, Second: 2 36 | // First: 2, Second: 3 37 | // First: 3, Second: 4 38 | // First: 4, Second: 5 39 | ``` 40 | 41 | It composes well with the [Dictionary.init(_:uniquingKeysWith:)](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md) API that deals with `AsyncSequence` of tuples. 42 | 43 | ```swift 44 | Dictionary(uniqueKeysWithValues: url.lines.adjacentPairs()) 45 | ``` 46 | 47 | ## Alternatives Considered 48 | 49 | This functionality is often written as a `zip` of a sequence together with itself, dropping its first element (`zip(source, source.dropFirst())`). 50 | 51 | It's such a dominant use-case, the [swift-algorithms](https://github.com/apple/swift-algorithms) package also [introduced](https://github.com/apple/swift-algorithms/pull/119) it to its collection of algorithms. 52 | 53 | ## Credits/Inspiration 54 | 55 | The synchronous counterpart in [swift-algorithms](https://github.com/apple/swift-algorithms/blob/main/Guides/AdjacentPairs.md). 56 | -------------------------------------------------------------------------------- /Evolution/0007-chain.md: -------------------------------------------------------------------------------- 1 | # Chain 2 | 3 | * Proposal: [SAA-0007](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0007-chain.md) 4 | * Authors: [Philippe Hausler](https://github.com/phausler) 5 | * Status: **Accepted** 6 | * 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) | 7 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChain.swift)] 8 | 9 | ## Introduction 10 | 11 | Combining asynchronous sequences can occur in multiple ways. One such way that is common for non asynchronous sequences is iterating a prefix sequence and then iterating a suffix sequence. The asynchronous version is just as useful and common. This algorithm has been dubbed `chain` in the swift algorithms package. 12 | 13 | The chain algorithm brings together two or more asynchronous sequences together sequentially where the elements from the resulting asynchronous sequence are comprised in order from the elements of the first asynchronous sequence and then the second (and so on) or until an error occurs. 14 | 15 | This operation is available for all `AsyncSequence` types who share the same `Element` type. 16 | 17 | ```swift 18 | let preamble = [ 19 | "// Some header to add as a preamble", 20 | "//", 21 | "" 22 | ].async 23 | let lines = chain(preamble, URL(fileURLWithPath: "/tmp/Sample.swift").lines) 24 | 25 | for try await line in lines { 26 | print(line) 27 | } 28 | ``` 29 | 30 | The above example shows how two `AsyncSequence` types can be chained together. In this case it prepends a preamble to the `lines` content of the file. 31 | 32 | ## Detailed Design 33 | 34 | This function family and the associated family of return types are prime candidates for variadic generics. Until that proposal is accepted, these will be implemented in terms of two- and three-base sequence cases. 35 | 36 | ```swift 37 | public func chain(_ s1: Base1, _ s2: Base2) -> AsyncChain2Sequence where Base1.Element == Base2.Element 38 | 39 | public func chain(_ s1: Base1, _ s2: Base2, _ s3: Base3) -> AsyncChain3Sequence 40 | 41 | public struct AsyncChain2Sequence where Base1.Element == Base2.Element { 42 | public typealias Element = Base1.Element 43 | 44 | public struct Iterator: AsyncIteratorProtocol { 45 | public mutating func next() async rethrows -> Element? 46 | } 47 | 48 | public func makeAsyncIterator() -> Iterator 49 | } 50 | 51 | extension AsyncChain2Sequence: Sendable where Base1: Sendable, Base2: Sendable { } 52 | extension AsyncChain2Sequence.Iterator: Sendable where Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { } 53 | 54 | public struct AsyncChain3Sequence where Base1.Element == Base2.Element, Base1.Element == Base3.Element { 55 | public typealias Element = Base1.Element 56 | 57 | public struct Iterator: AsyncIteratorProtocol { 58 | public mutating func next() async rethrows -> Element? 59 | } 60 | 61 | public func makeAsyncIterator() -> Iterator 62 | } 63 | 64 | extension AsyncChain3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable { } 65 | extension AsyncChain3Sequence.Iterator: Sendable where Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { } 66 | ``` 67 | 68 | The `chain(_:...)` function takes two or more sequences as arguments. 69 | 70 | The resulting `AsyncChainSequence` type is an asynchronous sequence, with conditional conformance to `Sendable` when the arguments also conform to it. 71 | 72 | When any of the asynchronous sequences being chained together come to their end of iteration, the `AsyncChainSequence` iteration proceeds to the next asynchronous sequence. When the last asynchronous sequence reaches the end of iteration, the `AsyncChainSequence` then ends its iteration. 73 | 74 | At any point in time, if one of the comprising asynchronous sequences throws an error during iteration, the resulting `AsyncChainSequence` iteration will throw that error and end iteration. The throwing behavior of `AsyncChainSequence` is that it will throw when any of its comprising bases throw, and will not throw when all of its comprising bases do not throw. 75 | 76 | ### Naming 77 | 78 | This function's and type's name match the term of art used in other languages and libraries. 79 | 80 | This combinator function is a direct analog to the synchronous version [defined in the Swift Algorithms package](https://github.com/apple/swift-algorithms/blob/main/Guides/Chain.md). 81 | -------------------------------------------------------------------------------- /Evolution/0008-bytes.md: -------------------------------------------------------------------------------- 1 | # AsyncBufferedByteIterator 2 | 3 | * Proposal: [SAA-0008](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0008-bytes.md) 4 | * Authors: [David Smith](https://github.com/Catfish-Man), [Philippe Hausler](https://github.com/phausler) 5 | * Status: **Accepted** 6 | * Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift) | 7 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift)] 8 | 9 | ## Introduction 10 | 11 | Sources of bytes are a common point of asynchrony; reading from files, reading from the network, or other such tasks. Having an easy to use, uniform, and performant utility to make this approachable is key to unlocking highly scalable byte handling. This has proven useful for `FileHandle`, `URL`, and a number of others in Foundation. 12 | 13 | This type provides infrastructure for creating `AsyncSequence` types with an `Element` of `UInt8` backed by file descriptors or similar read sources. 14 | 15 | ```swift 16 | struct AsyncBytes: AsyncSequence { 17 | public typealias Element = UInt8 18 | var handle: ReadableThing 19 | 20 | internal init(_ readable: ReadableThing) { 21 | handle = readable 22 | } 23 | 24 | public func makeAsyncIterator() -> AsyncBufferedByteIterator { 25 | return AsyncBufferedByteIterator(capacity: 16384) { buffer in 26 | // This runs once every 16384 invocations of next() 27 | return try await handle.read(into: buffer) 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | ## Detailed Design 34 | 35 | ```swift 36 | public struct AsyncBufferedByteIterator: AsyncIteratorProtocol { 37 | public typealias Element = UInt8 38 | 39 | public init( 40 | capacity: Int, 41 | readFunction: @Sendable @escaping (UnsafeMutableRawBufferPointer) async throws -> Int 42 | ) 43 | 44 | public mutating func next() async throws -> UInt8? 45 | } 46 | ``` 47 | 48 | For each invocation of `next`, the iterator will check if a buffer has been filled. If the buffer is filled with some amount of bytes, a fast path is taken to directly return a byte out of that buffer. If the buffer is not filled, the read function is invoked to acquire the next filled buffer, at which point it takes a byte out of that buffer. 49 | 50 | If the read function returns `0`, indicating it didn't read any more bytes, the iterator is decided to be finished and no additional invocations to the read function are made. 51 | 52 | If the read function throws, the error will be thrown by the iteration. Subsequent invocations to the iterator will then return `nil` without invoking the read function. 53 | 54 | If the task is cancelled during the iteration, the iteration will check the cancellation only in passes where the read function is invoked, and will throw a `CancellationError`. 55 | 56 | ### Naming 57 | 58 | This type was named precisely for what it does: it is an asynchronous iterator that buffers bytes. 59 | 60 | -------------------------------------------------------------------------------- /Evolution/0009-async.md: -------------------------------------------------------------------------------- 1 | # AsyncSyncSequence 2 | 3 | * Proposal: [SAA-0009](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0009-async.md) 4 | * Authors: [Philippe Hausler](https://github.com/phausler) 5 | * Status: **Implemented** 6 | 7 | * Implementation: 8 | [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncSyncSequence.swift) | 9 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestLazy.swift) 10 | 11 | ## Introduction 12 | 13 | `AsyncSyncSequence` converts a non-asynchronous sequence into an asynchronous one. 14 | 15 | This operation is available for all `Sequence` types. 16 | 17 | ```swift 18 | let numbers = [1, 2, 3, 4].async 19 | let characters = "abcde".async 20 | ``` 21 | 22 | This transformation can be useful to test operations specifically available on `AsyncSequence` but also is useful 23 | to combine with other `AsyncSequence` types to provide well known sources of data. 24 | 25 | The `.async` property returns an `AsyncSyncSequence` that is generic upon the base `Sequence` it was constructed from. 26 | 27 | ```swift 28 | extension Sequence { 29 | public var async: AsyncSyncSequence { get } 30 | } 31 | 32 | public struct AsyncSyncSequence: AsyncSequence { 33 | ... 34 | } 35 | 36 | extension AsyncSyncSequence: Sendable where Base: Sendable { } 37 | extension AsyncSyncSequence.Iterator: Sendable where Base.Iterator: Sendable { } 38 | ``` 39 | 40 | ### Naming 41 | 42 | This property's and type's name match the naming approaches in the Swift standard library. The property is named with a 43 | succinct name in inspiration from `.lazy`, and the type is named in reference to the lazy behavior of the constructed 44 | `AsyncSequence`. 45 | 46 | ## Effect on API resilience 47 | 48 | `AsyncSyncSequence` has a trivial implementation and is marked as `@frozen` and `@inlinable`. This removes the ability of this type and functions to be ABI resilient boundaries at the benefit of being highly optimizable. 49 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | 3 | import PackageDescription 4 | import CompilerPluginSupport 5 | 6 | // Availability Macros 7 | let availabilityTags = [Availability("AsyncAlgorithms")] 8 | let versionNumbers = ["1.0"] 9 | 10 | // Availability Macro Utilities 11 | enum OSAvailability: String { 12 | // This should match the package's deployment target 13 | case initialIntroduction = "macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0" 14 | case pending = "macOS 9999, iOS 9999, tvOS 9999, watchOS 9999" 15 | // Use 10000 for future availability to avoid compiler magic around 16 | // the 9999 version number but ensure it is greater than 9999 17 | case future = "macOS 10000, iOS 10000, tvOS 10000, watchOS 10000" 18 | } 19 | 20 | struct Availability { 21 | let name: String 22 | let osAvailability: OSAvailability 23 | 24 | init(_ name: String, availability: OSAvailability = .initialIntroduction) { 25 | self.name = name 26 | self.osAvailability = availability 27 | } 28 | } 29 | 30 | let availabilityMacros: [SwiftSetting] = versionNumbers.flatMap { version in 31 | availabilityTags.map { 32 | .enableExperimentalFeature("AvailabilityMacro=\($0.name) \(version):\($0.osAvailability.rawValue)") 33 | } 34 | } 35 | 36 | let package = Package( 37 | name: "swift-async-algorithms", 38 | products: [ 39 | .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]) 40 | ], 41 | targets: [ 42 | .target( 43 | name: "AsyncAlgorithms", 44 | dependencies: [ 45 | .product(name: "OrderedCollections", package: "swift-collections"), 46 | .product(name: "DequeModule", package: "swift-collections"), 47 | ], 48 | swiftSettings: availabilityMacros + [ 49 | .enableExperimentalFeature("StrictConcurrency=complete") 50 | ] 51 | ), 52 | .target( 53 | name: "AsyncSequenceValidation", 54 | dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"], 55 | swiftSettings: availabilityMacros + [ 56 | .enableExperimentalFeature("StrictConcurrency=complete") 57 | ] 58 | ), 59 | .systemLibrary(name: "_CAsyncSequenceValidationSupport"), 60 | .target( 61 | name: "AsyncAlgorithms_XCTest", 62 | dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"], 63 | swiftSettings: availabilityMacros + [ 64 | .enableExperimentalFeature("StrictConcurrency=complete") 65 | ] 66 | ), 67 | .testTarget( 68 | name: "AsyncAlgorithmsTests", 69 | dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"], 70 | swiftSettings: availabilityMacros + [ 71 | .enableExperimentalFeature("StrictConcurrency=complete") 72 | ] 73 | ), 74 | ] 75 | ) 76 | 77 | if Context.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { 78 | package.dependencies += [ 79 | .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), 80 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), 81 | ] 82 | } else { 83 | package.dependencies += [ 84 | .package(path: "../swift-collections") 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /Package@swift-5.7.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swift-async-algorithms", 7 | platforms: [ 8 | .macOS("10.15"), 9 | .iOS("13.0"), 10 | .tvOS("13.0"), 11 | .watchOS("6.0"), 12 | ], 13 | products: [ 14 | .library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"]), 15 | .library(name: "AsyncSequenceValidation", targets: ["AsyncSequenceValidation"]), 16 | .library(name: "_CAsyncSequenceValidationSupport", type: .static, targets: ["AsyncSequenceValidation"]), 17 | .library(name: "AsyncAlgorithms_XCTest", targets: ["AsyncAlgorithms_XCTest"]), 18 | ], 19 | dependencies: [ 20 | .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"), 21 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), 22 | ], 23 | targets: [ 24 | .target( 25 | name: "AsyncAlgorithms", 26 | dependencies: [.product(name: "Collections", package: "swift-collections")] 27 | ), 28 | .target( 29 | name: "AsyncSequenceValidation", 30 | dependencies: ["_CAsyncSequenceValidationSupport", "AsyncAlgorithms"] 31 | ), 32 | .systemLibrary(name: "_CAsyncSequenceValidationSupport"), 33 | .target( 34 | name: "AsyncAlgorithms_XCTest", 35 | dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"] 36 | ), 37 | .testTarget( 38 | name: "AsyncAlgorithmsTests", 39 | dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"] 40 | ), 41 | ] 42 | ) 43 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | extension AsyncSequence { 14 | /// An `AsyncSequence` that iterates over the adjacent pairs of the original 15 | /// original `AsyncSequence`. 16 | /// 17 | /// ``` 18 | /// for await (first, second) in (1...5).async.adjacentPairs() { 19 | /// print("First: \(first), Second: \(second)") 20 | /// } 21 | /// 22 | /// // First: 1, Second: 2 23 | /// // First: 2, Second: 3 24 | /// // First: 3, Second: 4 25 | /// // First: 4, Second: 5 26 | /// ``` 27 | /// 28 | /// - Returns: An `AsyncSequence` where the element is a tuple of two adjacent elements 29 | /// or the original `AsyncSequence`. 30 | @available(AsyncAlgorithms 1.0, *) 31 | @inlinable 32 | public func adjacentPairs() -> AsyncAdjacentPairsSequence { 33 | AsyncAdjacentPairsSequence(self) 34 | } 35 | } 36 | 37 | /// An `AsyncSequence` that iterates over the adjacent pairs of the original 38 | /// `AsyncSequence`. 39 | @available(AsyncAlgorithms 1.0, *) 40 | @frozen 41 | public struct AsyncAdjacentPairsSequence: AsyncSequence { 42 | public typealias Element = (Base.Element, Base.Element) 43 | 44 | @usableFromInline 45 | let base: Base 46 | 47 | @inlinable 48 | init(_ base: Base) { 49 | self.base = base 50 | } 51 | 52 | /// The iterator for an `AsyncAdjacentPairsSequence` instance. 53 | @frozen 54 | public struct Iterator: AsyncIteratorProtocol { 55 | public typealias Element = (Base.Element, Base.Element) 56 | 57 | @usableFromInline 58 | var base: Base.AsyncIterator 59 | 60 | @usableFromInline 61 | internal var previousElement: Base.Element? 62 | 63 | @inlinable 64 | init(_ base: Base.AsyncIterator) { 65 | self.base = base 66 | } 67 | 68 | @inlinable 69 | public mutating func next() async rethrows -> (Base.Element, Base.Element)? { 70 | if previousElement == nil { 71 | previousElement = try await base.next() 72 | } 73 | 74 | guard let previous = previousElement, let next = try await base.next() else { 75 | return nil 76 | } 77 | 78 | previousElement = next 79 | return (previous, next) 80 | } 81 | } 82 | 83 | @inlinable 84 | public func makeAsyncIterator() -> Iterator { 85 | Iterator(base.makeAsyncIterator()) 86 | } 87 | } 88 | 89 | @available(AsyncAlgorithms 1.0, *) 90 | extension AsyncAdjacentPairsSequence: Sendable where Base: Sendable, Base.Element: Sendable {} 91 | 92 | @available(*, unavailable) 93 | extension AsyncAdjacentPairsSequence.Iterator: Sendable {} 94 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncAlgorithms.docc/AsyncAlgorithms.md: -------------------------------------------------------------------------------- 1 | # ``AsyncAlgorithms`` 2 | 3 | **Swift Async Algorithms** is an open-source package of asynchronous sequence and advanced algorithms that involve concurrency, along with their related types. 4 | 5 | ## Overview 6 | 7 | This package has three main goals: 8 | 9 | - First-class integration with `async/await` 10 | - Provide a home for time-based algorithms 11 | - Be cross-platform and open source 12 | 13 | ## Topics 14 | 15 | ### Getting Started 16 | 17 | - 18 | - 19 | - 20 | - 21 | - 22 | - 23 | - 24 | - 25 | - 26 | - 27 | - 28 | - 29 | - 30 | - 31 | - 32 | - 33 | - 34 | - 35 | - 36 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/AdjacentPairs.md: -------------------------------------------------------------------------------- 1 | # AdjacentPairs 2 | 3 | * Author(s): [László Teveli](https://github.com/tevelee) 4 | 5 | [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift) | 6 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift)] 7 | 8 | The `adjacentPairs()` API serve the purpose of collecting adjacent values. This operation is available for any `AsyncSequence` by calling the `adjacentPairs()` method. 9 | 10 | ```swift 11 | extension AsyncSequence { 12 | public func adjacentPairs() -> AsyncAdjacentPairsSequence 13 | } 14 | ``` 15 | 16 | ## Detailed Design 17 | 18 | The `adjacentPairs()` algorithm produces elements of tuple (size of 2), containing a pair of the original `Element` type. 19 | 20 | The interface for this algorithm is available on all `AsyncSequence` types. The returned `AsyncAdjacentPairsSequence` conditionally conforms to `Sendable`. 21 | 22 | Its iterator keeps track of the previous element returned in the `next()` function and updates it in every turn. 23 | 24 | ```swift 25 | for await (first, second) in (1...5).async.adjacentPairs() { 26 | print("First: \(first), Second: \(second)") 27 | } 28 | 29 | // First: 1, Second: 2 30 | // First: 2, Second: 3 31 | // First: 3, Second: 4 32 | // First: 4, Second: 5 33 | ``` 34 | 35 | It composes well with the [Dictionary.init(_:uniquingKeysWith:)](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md) API that deals with `AsyncSequence` of tuples. 36 | 37 | ```swift 38 | Dictionary(uniqueKeysWithValues: url.lines.adjacentPairs()) 39 | ``` 40 | 41 | ## Alternatives Considered 42 | 43 | This functionality is often written as a `zip` of a sequence together with itself, dropping its first element (`zip(source, source.dropFirst())`). 44 | 45 | It's such a dominant use-case, the [swift-algorithms](https://github.com/apple/swift-algorithms) package also [introduced](https://github.com/apple/swift-algorithms/pull/119) it to its collection of algorithms. 46 | 47 | ## Credits/Inspiration 48 | 49 | The synchronous counterpart in [swift-algorithms](https://github.com/apple/swift-algorithms/blob/main/Guides/AdjacentPairs.md). 50 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/BufferedBytes.md: -------------------------------------------------------------------------------- 1 | # AsyncBufferedByteIterator 2 | 3 | Provides a highly efficient iterator useful for iterating byte sequences derived from asynchronous read functions. 4 | 5 | [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift) | 6 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift)] 7 | 8 | This type provides infrastructure for creating `AsyncSequence` types with an `Element` of `UInt8` backed by file descriptors or similar read sources. 9 | 10 | ```swift 11 | struct AsyncBytes: AsyncSequence { 12 | public typealias Element = UInt8 13 | var handle: ReadableThing 14 | 15 | internal init(_ readable: ReadableThing) { 16 | handle = readable 17 | } 18 | 19 | public func makeAsyncIterator() -> AsyncBufferedByteIterator { 20 | return AsyncBufferedByteIterator(capacity: 16384) { buffer in 21 | // This runs once every 16384 invocations of next() 22 | return try await handle.read(into: buffer) 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | ## Detailed Design 29 | 30 | ```swift 31 | public struct AsyncBufferedByteIterator: AsyncIteratorProtocol, Sendable { 32 | public typealias Element = UInt8 33 | 34 | public init( 35 | capacity: Int, 36 | readFunction: @Sendable @escaping (UnsafeMutableRawBufferPointer) async throws -> Int 37 | ) 38 | 39 | public mutating func next() async throws -> UInt8? 40 | } 41 | ``` 42 | 43 | For each invocation of `next`, the iterator will check if a buffer has been filled. If the buffer is filled with some amount of bytes, a fast path is taken to directly return a byte out of that buffer. If the buffer is not filled, the read function is invoked to acquire the next filled buffer, at which point it takes a byte out of that buffer. 44 | 45 | If the read function returns `0`, indicating it didn't read any more bytes, the iterator is decided to be finished and no additional invocations to the read function are made. 46 | 47 | If the read function throws, the error will be thrown by the iteration. Subsequent invocations to the iterator will then return `nil` without invoking the read function. 48 | 49 | If the task is cancelled during the iteration, the iteration will check the cancellation only in passes where the read function is invoked, and will throw a `CancellationError`. 50 | 51 | ### Naming 52 | 53 | This type was named precisely for what it does: it is an asynchronous iterator that buffers bytes. 54 | 55 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Chain.md: -------------------------------------------------------------------------------- 1 | # Chain 2 | 3 | Chains two or more asynchronous sequences together sequentially. 4 | 5 | [[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) | 6 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChain.swift)] 7 | 8 | Chains two or more asynchronous sequences together sequentially where the elements from the resulting asynchronous sequence are comprised in order from the elements of the first asynchronous sequence and then the second (and so on) or until an error occurs. 9 | 10 | This operation is available for all `AsyncSequence` types who share the same `Element` type. 11 | 12 | ```swift 13 | let preamble = [ 14 | "// Some header to add as a preamble", 15 | "//", 16 | "" 17 | ].async 18 | let lines = chain(preamble, URL(fileURLWithPath: "/tmp/Sample.swift").lines) 19 | 20 | for try await line in lines { 21 | print(line) 22 | } 23 | ``` 24 | 25 | The above example shows how two `AsyncSequence` types can be chained together. In this case it prepends a preamble to the `lines` content of the file. 26 | 27 | ## Detailed Design 28 | 29 | This function family and the associated family of return types are prime candidates for variadic generics. Until that proposal is accepted, these will be implemented in terms of two- and three-base sequence cases. 30 | 31 | ```swift 32 | public func chain(_ s1: Base1, _ s2: Base2) -> AsyncChain2Sequence where Base1.Element == Base2.Element 33 | 34 | public func chain(_ s1: Base1, _ s2: Base2, _ s3: Base3) -> AsyncChain3Sequence 35 | 36 | public struct AsyncChain2Sequence where Base1.Element == Base2.Element { 37 | public typealias Element = Base1.Element 38 | 39 | public struct Iterator: AsyncIteratorProtocol { 40 | public mutating func next() async rethrows -> Element? 41 | } 42 | 43 | public func makeAsyncIterator() -> Iterator 44 | } 45 | 46 | extension AsyncChain2Sequence: Sendable where Base1: Sendable, Base2: Sendable { } 47 | extension AsyncChain2Sequence.Iterator: Sendable where Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { } 48 | 49 | public struct AsyncChain3Sequence where Base1.Element == Base2.Element, Base1.Element == Base3.Element { 50 | public typealias Element = Base1.Element 51 | 52 | public struct Iterator: AsyncIteratorProtocol { 53 | public mutating func next() async rethrows -> Element? 54 | } 55 | 56 | public func makeAsyncIterator() -> Iterator 57 | } 58 | 59 | extension AsyncChain3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable { } 60 | extension AsyncChain3Sequence.Iterator: Sendable where Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { } 61 | ``` 62 | 63 | The `chain(_:...)` function takes two or more sequences as arguments. 64 | 65 | The resulting `AsyncChainSequence` type is an asynchronous sequence, with conditional conformance to `Sendable` when the arguments also conform to it. 66 | 67 | When any of the asynchronous sequences being chained together come to their end of iteration, the `AsyncChainSequence` iteration proceeds to the next asynchronous sequence. When the last asynchronous sequence reaches the end of iteration, the `AsyncChainSequence` then ends its iteration. 68 | 69 | At any point in time, if one of the comprising asynchronous sequences throws an error during iteration, the resulting `AsyncChainSequence` iteration will throw that error and end iteration. The throwing behavior of `AsyncChainSequence` is that it will throw when any of its comprising bases throw, and will not throw when all of its comprising bases do not throw. 70 | 71 | ### Naming 72 | 73 | This function's and type's name match the term of art used in other languages and libraries. 74 | 75 | This combinator function is a direct analog to the synchronous version [defined in the Swift Algorithms package](https://github.com/apple/swift-algorithms/blob/main/Guides/Chain.md). 76 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Compacted.md: -------------------------------------------------------------------------------- 1 | # Compacted 2 | 3 | * Author(s): [Philippe Hausler](https://github.com/phausler) 4 | 5 | [ 6 | [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncCompactedSequence.swift) | 7 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestCompacted.swift) 8 | ] 9 | 10 | ## Introduction 11 | 12 | Just as it is common for `Sequence` types that contain optional values to need to `.compactMap { $0 }`, `AsyncSequence` types have the same use cases. This common task means that the type must employ a closure to test the optional value. This can be done more efficiently for both execution performance as well as API efficiency of typing. 13 | 14 | ## Proposed Solution 15 | 16 | Similar to the Swift Algorithms package we propose that a new method be added to `AsyncSequence` to fit this need. 17 | 18 | ```swift 19 | extension AsyncSequence { 20 | public func compacted() -> AsyncCompactedSequence 21 | where Element == Unwrapped? 22 | } 23 | ``` 24 | 25 | This is equivalent to writing `.compactMap { $0 }` from a behavioral standpoint but is easier to reason about and is more efficient since it does not need to execute or store a closure. 26 | 27 | ## Detailed Design 28 | 29 | The `AsyncCompactedSequence` type from an effects standpoint works just like `AsyncCompactMapSequence`. When the base asynchronous sequence throws, the iteration of `AsyncCompactedSequence` can throw. Likewise if the base does not throw then the iteration of `AsyncCompactedSequence` does not throw. This type is conditionally `Sendable` when the base, base element, and base iterator are `Sendable. 30 | 31 | ```swift 32 | public struct AsyncCompactedSequence: AsyncSequence 33 | where Base.Element == Element? { 34 | 35 | public struct Iterator: AsyncIteratorProtocol { 36 | public mutating func next() async rethrows -> Element? 37 | } 38 | 39 | public func makeAsyncIterator() -> Iterator { 40 | Iterator(base.makeAsyncIterator()) 41 | } 42 | } 43 | 44 | extension AsyncCompactedSequence: Sendable 45 | where 46 | Base: Sendable, Base.Element: Sendable, 47 | Base.AsyncIterator: Sendable { } 48 | 49 | extension AsyncCompactedSequence.Iterator: Sendable 50 | where 51 | Base: Sendable, Base.Element: Sendable, 52 | Base.AsyncIterator: Sendable { } 53 | ``` 54 | 55 | ## Credits/Inspiration 56 | 57 | This transformation function is a direct analog to the synchronous version [defined in the Swift Algorithms package](https://github.com/apple/swift-algorithms/blob/main/Guides/Compacted.md) 58 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Debounce.md: -------------------------------------------------------------------------------- 1 | # Debounce 2 | 3 | * Author(s): [Philippe Hausler](https://github.com/phausler) 4 | 5 | [ 6 | [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift) | 7 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestDebounce.swift) 8 | ] 9 | 10 | ## Introduction 11 | 12 | When events can potentially happen faster than the desired consumption rate, there are multiple ways to handle the situation. One approach is to only emit values after a given period of time of inactivity, or "quiescence", has elapsed. This algorithm is commonly referred to as debouncing. 13 | 14 | ## Proposed Solution 15 | 16 | The debounce algorithm produces elements after a particular duration has passed between events. It transacts within a given tolerance applied to a clock. If values are produced by the base `AsyncSequence` during this quiet period, the debounce does not resume its next iterator until the period has elapsed with no values are produced or unless a terminal event is encountered. 17 | 18 | The interface for this algorithm is available on all `AsyncSequence` types where the base type, iterator, and element are `Sendable`, since this algorithm will inherently create tasks to manage their timing of events. A shorthand implementation will be offered where the clock is the `ContinuousClock`, which allows for easy construction with `Duration` values. 19 | 20 | ```swift 21 | extension AsyncSequence { 22 | public func debounce( 23 | for interval: C.Instant.Duration, 24 | tolerance: C.Instant.Duration? = nil, 25 | clock: C 26 | ) -> AsyncDebounceSequence 27 | 28 | public func debounce( 29 | for interval: Duration, 30 | tolerance: Duration? = nil 31 | ) -> AsyncDebounceSequence 32 | } 33 | ``` 34 | 35 | This all boils down to a terse description of how to transform the asynchronous sequence over time. 36 | 37 | ```swift 38 | fastEvents.debounce(for: .seconds(1)) 39 | ``` 40 | 41 | In this case it transforms a potentially fast asynchronous sequence of events into one that waits for a window of 1 second with no events to elapse before emitting a value. 42 | 43 | ## Detailed Design 44 | 45 | The type that implements the algorithm for debounce emits the same element type as the base that it applies to. It also throws when the base type throws (and likewise does not throw when the base type does not throw). 46 | 47 | ```swift 48 | public struct AsyncDebounceSequence: Sendable 49 | where Base.AsyncIterator: Sendable, Base.Element: Sendable, Base: Sendable { 50 | } 51 | 52 | extension AsyncDebounceSequence: AsyncSequence { 53 | public typealias Element = Base.Element 54 | 55 | public struct Iterator: AsyncIteratorProtocol, Sendable { 56 | public mutating func next() async rethrows -> Base.Element? 57 | } 58 | 59 | public func makeAsyncIterator() -> Iterator 60 | } 61 | ``` 62 | 63 | Since the stored types comprising `AsyncDebounceSequence` must be `Sendable`; `AsyncDebounceSequence` is unconditionally always `Sendable`. 64 | 65 | ## Alternatives Considered 66 | 67 | An alternative form of `debounce` could exist similar to the reductions of `throttle`, where a closure would be invoked for each value being set as the latest, and reducing a new value to produce for the debounce. 68 | 69 | ## Credits/Inspiration 70 | 71 | The naming for debounce comes as a term of art; originally this term was inspired by electronic circuitry. When a physical switch closes a circuit it can easily have a "bouncing" behavior (also called chatter) that is caused by electrical contact resistance and the physical bounce of springs associated with switches. That phenomenon is often addressed with additional circuits to de-bounce (removing the bouncing) by ensuring a certain quiescence occurs. 72 | 73 | http://reactivex.io/documentation/operators/debounce.html 74 | 75 | https://developer.apple.com/documentation/combine/publishers/debounce/ 76 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Intersperse.md: -------------------------------------------------------------------------------- 1 | # Intersperse 2 | 3 | Places a given value in between each element of the asynchronous sequence. 4 | 5 | [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift) | 6 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestInterspersed.swift)] 7 | 8 | ```swift 9 | let numbers = [1, 2, 3].async.interspersed(with: 0) 10 | for await number in numbers { 11 | print(number) 12 | } 13 | // prints 1 0 2 0 3 14 | 15 | let empty = [].async.interspersed(with: 0) 16 | // await Array(empty) == [] 17 | ``` 18 | 19 | `interspersed(with:)` takes a separator value and inserts it in between every 20 | element in the asynchronous sequence. 21 | 22 | ## Detailed Design 23 | 24 | A new method is added to `AsyncSequence`: 25 | 26 | ```swift 27 | extension AsyncSequence { 28 | func interspersed(with separator: Element) -> AsyncInterspersedSequence 29 | } 30 | ``` 31 | 32 | The new `AsyncInterspersedSequence` type represents the asynchronous sequence 33 | when the separator is inserted between each element. 34 | 35 | When the base asynchronous sequence can throw on iteration, `AsyncInterspersedSequence` 36 | will throw on iteration. When the base does not throw, the iteration of 37 | `AsyncInterspersedSequence` does not throw either. 38 | 39 | `AsyncInterspersedSequence` is conditionally `Sendable` when the base asynchronous 40 | sequence is `Sendable` and the element is also `Sendable`. 41 | 42 | ### Naming 43 | 44 | This method’s and type’s name match the term of art used in other languages 45 | and libraries. 46 | 47 | This method is a direct analog to the synchronous version [defined in the Swift Algorithms package](https://github.com/apple/swift-algorithms/blob/main/Guides/Intersperse.md). 48 | 49 | ### Comparison with other languages 50 | 51 | **[Haskell][Haskell]:** Has an `intersperse` function which takes an element 52 | and a list and 'intersperses' that element between the elements of the list. 53 | 54 | **[Rust][Rust]:** Has a function called `intersperse` to insert a particular 55 | value between each element. 56 | 57 | 58 | 59 | [Haskell]: https://hackage.haskell.org/package/base-4.14.0.0/docs/Data-List.html#v:intersperse 60 | [Rust]: https://docs.rs/itertools/0.9.0/itertools/trait.Itertools.html#method.intersperse 61 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Lazy.md: -------------------------------------------------------------------------------- 1 | # AsyncSyncSequence 2 | 3 | This operation is available for all `Sequence` types. 4 | 5 | [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncSyncSequence.swift) | 6 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestLazy.swift)] 7 | 8 | ```swift 9 | let numbers = [1, 2, 3, 4].async 10 | let characters = "abcde".async 11 | ``` 12 | 13 | This transformation can be useful to test operations specifically available on `AsyncSequence` but also is useful 14 | to combine with other `AsyncSequence` types to provide well known sources of data. 15 | 16 | ## Detailed Design 17 | 18 | The `.async` property returns an `AsyncSyncSequence` that is generic upon the base `Sequence` it was constructed from. 19 | 20 | ```swift 21 | extension Sequence { 22 | public var async: AsyncSyncSequence { get } 23 | } 24 | 25 | public struct AsyncSyncSequence: AsyncSequence { 26 | ... 27 | } 28 | 29 | extension AsyncSyncSequence: Sendable where Base: Sendable { } 30 | extension AsyncSyncSequence.Iterator: Sendable where Base.Iterator: Sendable { } 31 | ``` 32 | 33 | ### Naming 34 | 35 | This property's and type's name match the naming approaches in the Swift standard library. The property is named with a 36 | succinct name in inspiration from `.lazy`, and the type is named in reference to the lazy behavior of the constructed 37 | `AsyncSequence`. 38 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md: -------------------------------------------------------------------------------- 1 | # Merge 2 | 3 | Merges two or more asynchronous sequences sharing the same element type into one singular asynchronous sequence. 4 | 5 | [[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) | 6 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestMerge.swift)] 7 | 8 | ```swift 9 | let appleFeed = URL(string: "http://www.example.com/ticker?symbol=AAPL")!.lines.map { "AAPL: " + $0 } 10 | let nasdaqFeed = URL(string:"http://www.example.com/ticker?symbol=^IXIC")!.lines.map { "^IXIC: " + $0 } 11 | 12 | for try await ticker in merge(appleFeed, nasdaqFeed) { 13 | print(ticker) 14 | } 15 | ``` 16 | 17 | Given some sample inputs the following merged events can be expected. 18 | 19 | | Timestamp | appleFeed | nasdaqFeed | merged output | 20 | | ----------- | --------- | ---------- | --------------- | 21 | | 11:40 AM | 173.91 | | AAPL: 173.91 | 22 | | 12:25 AM | | 14236.78 | ^IXIC: 14236.78 | 23 | | 12:40 AM | | 14218.34 | ^IXIC: 14218.34 | 24 | | 1:15 PM | 173.00 | | AAPL: 173.00 | 25 | 26 | ## Detailed Design 27 | 28 | This function family and the associated family of return types are prime candidates for variadic generics. Until that proposal is accepted, these will be implemented in terms of two- and three-base sequence cases. 29 | 30 | ```swift 31 | public func merge(_ base1: Base1, _ base2: Base2) -> AsyncMerge2Sequence 32 | 33 | public func merge(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncMerge3Sequence 34 | 35 | public struct AsyncMerge2Sequence: Sendable 36 | where 37 | Base1.Element == Base2.Element, 38 | Base1: Sendable, Base2: Sendable, 39 | Base1.Element: Sendable, Base2.Element: Sendable, 40 | Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { 41 | public typealias Element = Base1.Element 42 | 43 | public struct Iterator: AsyncIteratorProtocol { 44 | public mutating func next() async rethrows -> Element? 45 | } 46 | 47 | public func makeAsyncIterator() -> Iterator 48 | } 49 | 50 | public struct AsyncMerge3Sequence: Sendable 51 | where 52 | Base1.Element == Base2.Element, Base1.Element == Base3.Element, 53 | Base1: Sendable, Base2: Sendable, Base3: Sendable 54 | Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable 55 | Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { 56 | public typealias Element = Base1.Element 57 | 58 | public struct Iterator: AsyncIteratorProtocol { 59 | public mutating func next() async rethrows -> Element? 60 | } 61 | 62 | public func makeAsyncIterator() -> Iterator 63 | } 64 | 65 | ``` 66 | 67 | The `merge(_:...)` function takes two or more asynchronous sequences as arguments and produces an `AsyncMergeSequence` which is an asynchronous sequence. 68 | 69 | Since the bases comprising the `AsyncMergeSequence` must be iterated concurrently to produce the latest value, those sequences must be able to be sent to child tasks. This means that a prerequisite of the bases must be that the base asynchronous sequences, their iterators, and the elements they produce must be `Sendable`. 70 | 71 | When iterating a `AsyncMergeSequence`, the sequence terminates when all of the base asynchronous sequences terminate, since this means there is no potential for any further elements to be produced. 72 | 73 | The throwing behavior of `AsyncMergeSequence` is that if any of the bases throw, then the composed asynchronous sequence throws on its iteration. If at any point an error is thrown by any base, the other iterations are cancelled and the thrown error is immediately thrown to the consuming iteration. 74 | 75 | ### Naming 76 | 77 | Since the inherent behavior of `merge(_:...)` merges values from multiple streams into a singular asynchronous sequence, the naming is intended to be quite literal. There are precedent terms of art in other frameworks and libraries (listed in the comparison section). Other naming takes the form of "withLatestFrom". This was disregarded since the "with" prefix is often most associated with the passing of a closure and some sort of contextual concept; `withUnsafePointer` or `withUnsafeContinuation` are prime examples. 78 | 79 | ### Comparison with other libraries 80 | 81 | **ReactiveX** ReactiveX has an [API definition of Merge](https://reactivex.io/documentation/operators/merge.html) as a top level function for merging Observables. 82 | 83 | **Combine** Combine has an [API definition of merge(with:)](https://developer.apple.com/documentation/combine/publisher/merge(with:)-7qt71/) as an operator style method for merging Publishers. 84 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Throttle.md: -------------------------------------------------------------------------------- 1 | # Throttle 2 | 3 | * Author(s): [Philippe Hausler](https://github.com/phausler) 4 | 5 | [ 6 | [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncThrottleSequence.swift) | 7 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestThrottle.swift) 8 | ] 9 | 10 | ## Introduction 11 | 12 | When events can potentially happen faster than the desired consumption rate, there are multiple ways to handle the situation. One approach is to emit values after a given period has elapsed. These emitted values can be reduced from the values encountered during the waiting period. This algorithm is commonly referred to as throttling. 13 | 14 | ## Proposed Solution 15 | 16 | The throttle algorithm produces elements such that at least a specific interval has elapsed between them. It transacts by measuring against a specific clock. If values are produced by the base `AsyncSequence` the throttle does not resume its next iterator until the period has elapsed or unless a terminal event is encountered. 17 | 18 | The interface for this algorithm is available on all `AsyncSequence` types. Unlike other algorithms like `debounce`, the throttle algorithm does not need to create additional tasks or require any sort of tolerance because the interval is just measured. A shorthand implementation will be offered in conjunction where the clock is the `ContinuousClock`, which allows for easy construction with `Duration` values. An additional shorthand is offered to reduce the values such that it provides a "latest" or "earliest" value, representing the leading or trailing edge of a throttled region of production of events. 19 | 20 | ```swift 21 | extension AsyncSequence { 22 | public func throttle( 23 | for interval: C.Instant.Duration, 24 | clock: C, 25 | reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced 26 | ) -> AsyncThrottleSequence 27 | 28 | public func throttle( 29 | for interval: Duration, 30 | reducing: @Sendable @escaping (Reduced?, Element) async -> Reduced 31 | ) -> AsyncThrottleSequence 32 | 33 | public func throttle( 34 | for interval: C.Instant.Duration, 35 | clock: C, 36 | latest: Bool = true 37 | ) -> AsyncThrottleSequence 38 | 39 | public func throttle( 40 | for interval: Duration, 41 | latest: Bool = true 42 | ) -> AsyncThrottleSequence 43 | } 44 | ``` 45 | 46 | This all boils down to a terse description of how to transform the asynchronous sequence over time. 47 | 48 | ```swift 49 | fastEvents.throttle(for: .seconds(1)) 50 | ``` 51 | 52 | In this case, the throttle transforms a potentially fast asynchronous sequence of events into one that waits for a window of 1 second to elapse before emitting a value. 53 | 54 | ## Detailed Design 55 | 56 | The type that implements the algorithm for throttle emits the same element type as the base that it applies to. It also throws when the base type throws (and likewise does not throw when the base type does not throw). 57 | 58 | ```swift 59 | public struct AsyncThrottleSequence { 60 | } 61 | 62 | extension AsyncThrottleSequence: AsyncSequence { 63 | public typealias Element = Reduced 64 | 65 | public struct Iterator: AsyncIteratorProtocol { 66 | public mutating func next() async rethrows -> Reduced? 67 | } 68 | 69 | public func makeAsyncIterator() -> Iterator 70 | } 71 | 72 | extension AsyncThrottleSequence: Sendable 73 | where Base: Sendable, Element: Sendable { } 74 | extension AsyncThrottleSequence.Iterator: Sendable 75 | where Base.AsyncIterator: Sendable { } 76 | ``` 77 | 78 | The `AsyncThrottleSequence` and its `Iterator` are conditionally `Sendable` if the base types comprising it are `Sendable`. 79 | 80 | The time in which events are measured are from the previous emission if present. If a duration has elapsed between the last emission and the point in time the throttle is measured then that duration is counted as elapsed. The first element is considered not throttled because no interval can be constructed from the start to the first element. 81 | 82 | ## Alternatives Considered 83 | 84 | It was considered to only provide the "latest" style APIs, however the reduction version grants more flexibility and can act as a funnel to the implementations of `latest`. 85 | 86 | ## Credits/Inspiration 87 | 88 | http://reactivex.io/documentation/operators/sample.html 89 | 90 | https://developer.apple.com/documentation/combine/publishers/throttle/ 91 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Timer.md: -------------------------------------------------------------------------------- 1 | # Timer 2 | 3 | * Author(s): [Philippe Hausler](https://github.com/phausler) 4 | 5 | [ 6 | [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncTimerSequence.swift) | 7 | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestTimer.swift) 8 | ] 9 | 10 | ## Introduction 11 | 12 | Producing elements at regular intervals can be useful for composing with other algorithms. These can range from invoking code at specific times to using those regular intervals as a delimiter of events. There are other cases this exists in APIs however those do not currently interact with Swift concurrency. These existing APIs are ones like `Timer` or `DispatchTimer` but are bound to internal clocks that are not extensible. 13 | 14 | ## Proposed Solution 15 | 16 | We propose to add a new type; `AsyncTimerSequence` which utilizes the new `Clock`, `Instant` and `Duration` types. This allows the interaction of the timer to custom implementations of types adopting `Clock`. 17 | 18 | This asynchronous sequence will produce elements of the clock's `Instant` type after the interval has elapsed. That instant will be the `now` at the time that the sleep has resumed. For each invocation to `next()` the `AsyncTimerSequence.Iterator` will calculate the next deadline to resume and pass that and the tolerance to the clock. If at any point in time the task executing that iteration is cancelled the iteration will return `nil` from the call to `next()`. 19 | 20 | ```swift 21 | public struct AsyncTimerSequence: AsyncSequence { 22 | public typealias Element = C.Instant 23 | 24 | public struct Iterator: AsyncIteratorProtocol { 25 | public mutating func next() async -> C.Instant? 26 | } 27 | 28 | public init( 29 | interval: C.Instant.Duration, 30 | tolerance: C.Instant.Duration? = nil, 31 | clock: C 32 | ) 33 | 34 | public func makeAsyncIterator() -> Iterator 35 | } 36 | 37 | extension AsyncTimerSequence where C == SuspendingClock { 38 | public static func repeating(every interval: Duration, tolerance: Duration? = nil) -> AsyncTimerSequence 39 | } 40 | 41 | extension AsyncTimerSequence: Sendable { } 42 | extension AsyncTimerSequence.Iterator: Sendable { } 43 | ``` 44 | 45 | Since all the types comprising `AsyncTimerSequence` are `Sendable` these types are also `Sendable`. 46 | 47 | ## Credits/Inspiration 48 | 49 | https://developer.apple.com/documentation/foundation/timer 50 | 51 | https://developer.apple.com/documentation/foundation/timer/timerpublisher 52 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncChain2Sequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | /// Returns a new asynchronous sequence that iterates over the two given asynchronous sequences, one 13 | /// followed by the other. 14 | /// 15 | /// - Parameters: 16 | /// - s1: The first asynchronous sequence. 17 | /// - s2: The second asynchronous sequence. 18 | /// - Returns: An asynchronous sequence that iterates first over the elements of `s1`, and 19 | /// then over the elements of `s2`. 20 | @available(AsyncAlgorithms 1.0, *) 21 | @inlinable 22 | public func chain( 23 | _ s1: Base1, 24 | _ s2: Base2 25 | ) -> AsyncChain2Sequence where Base1.Element == Base2.Element { 26 | AsyncChain2Sequence(s1, s2) 27 | } 28 | 29 | /// A concatenation of two asynchronous sequences with the same element type. 30 | @available(AsyncAlgorithms 1.0, *) 31 | @frozen 32 | public struct AsyncChain2Sequence where Base1.Element == Base2.Element { 33 | @usableFromInline 34 | let base1: Base1 35 | 36 | @usableFromInline 37 | let base2: Base2 38 | 39 | @usableFromInline 40 | init(_ base1: Base1, _ base2: Base2) { 41 | self.base1 = base1 42 | self.base2 = base2 43 | } 44 | } 45 | 46 | @available(AsyncAlgorithms 1.0, *) 47 | extension AsyncChain2Sequence: AsyncSequence { 48 | public typealias Element = Base1.Element 49 | 50 | /// The iterator for a `AsyncChain2Sequence` instance. 51 | @available(AsyncAlgorithms 1.0, *) 52 | @frozen 53 | public struct Iterator: AsyncIteratorProtocol { 54 | @usableFromInline 55 | var base1: Base1.AsyncIterator? 56 | 57 | @usableFromInline 58 | var base2: Base2.AsyncIterator? 59 | 60 | @usableFromInline 61 | init(_ base1: Base1.AsyncIterator, _ base2: Base2.AsyncIterator) { 62 | self.base1 = base1 63 | self.base2 = base2 64 | } 65 | 66 | @inlinable 67 | public mutating func next() async rethrows -> Element? { 68 | do { 69 | if let value = try await base1?.next() { 70 | return value 71 | } else { 72 | base1 = nil 73 | } 74 | return try await base2?.next() 75 | } catch { 76 | base1 = nil 77 | base2 = nil 78 | throw error 79 | } 80 | } 81 | } 82 | 83 | @available(AsyncAlgorithms 1.0, *) 84 | @inlinable 85 | public func makeAsyncIterator() -> Iterator { 86 | Iterator(base1.makeAsyncIterator(), base2.makeAsyncIterator()) 87 | } 88 | } 89 | 90 | @available(AsyncAlgorithms 1.0, *) 91 | extension AsyncChain2Sequence: Sendable where Base1: Sendable, Base2: Sendable {} 92 | 93 | @available(*, unavailable) 94 | extension AsyncChain2Sequence.Iterator: Sendable {} 95 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncChain3Sequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | /// Returns a new asynchronous sequence that iterates over the three given asynchronous sequences, one 13 | /// followed by the other. 14 | /// 15 | /// - Parameters: 16 | /// - s1: The first asynchronous sequence. 17 | /// - s2: The second asynchronous sequence. 18 | /// - s3: The third asynchronous sequence. 19 | /// - Returns: An asynchronous sequence that iterates first over the elements of `s1`, and 20 | /// then over the elements of `s2`, and then over the elements of `s3` 21 | @available(AsyncAlgorithms 1.0, *) 22 | @inlinable 23 | public func chain( 24 | _ s1: Base1, 25 | _ s2: Base2, 26 | _ s3: Base3 27 | ) -> AsyncChain3Sequence { 28 | AsyncChain3Sequence(s1, s2, s3) 29 | } 30 | 31 | /// A concatenation of three asynchronous sequences with the same element type. 32 | @available(AsyncAlgorithms 1.0, *) 33 | @frozen 34 | public struct AsyncChain3Sequence 35 | where Base1.Element == Base2.Element, Base1.Element == Base3.Element { 36 | @usableFromInline 37 | let base1: Base1 38 | 39 | @usableFromInline 40 | let base2: Base2 41 | 42 | @usableFromInline 43 | let base3: Base3 44 | 45 | @usableFromInline 46 | init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { 47 | self.base1 = base1 48 | self.base2 = base2 49 | self.base3 = base3 50 | } 51 | } 52 | 53 | @available(AsyncAlgorithms 1.0, *) 54 | extension AsyncChain3Sequence: AsyncSequence { 55 | public typealias Element = Base1.Element 56 | 57 | /// The iterator for a `AsyncChain3Sequence` instance. 58 | @available(AsyncAlgorithms 1.0, *) 59 | @frozen 60 | public struct Iterator: AsyncIteratorProtocol { 61 | @usableFromInline 62 | var base1: Base1.AsyncIterator? 63 | 64 | @usableFromInline 65 | var base2: Base2.AsyncIterator? 66 | 67 | @usableFromInline 68 | var base3: Base3.AsyncIterator? 69 | 70 | @usableFromInline 71 | init(_ base1: Base1.AsyncIterator, _ base2: Base2.AsyncIterator, _ base3: Base3.AsyncIterator) { 72 | self.base1 = base1 73 | self.base2 = base2 74 | self.base3 = base3 75 | } 76 | 77 | @inlinable 78 | public mutating func next() async rethrows -> Element? { 79 | do { 80 | if let value = try await base1?.next() { 81 | return value 82 | } else { 83 | base1 = nil 84 | } 85 | if let value = try await base2?.next() { 86 | return value 87 | } else { 88 | base2 = nil 89 | } 90 | return try await base3?.next() 91 | } catch { 92 | base1 = nil 93 | base2 = nil 94 | base3 = nil 95 | throw error 96 | } 97 | } 98 | } 99 | 100 | @available(AsyncAlgorithms 1.0, *) 101 | @inlinable 102 | public func makeAsyncIterator() -> Iterator { 103 | Iterator(base1.makeAsyncIterator(), base2.makeAsyncIterator(), base3.makeAsyncIterator()) 104 | } 105 | } 106 | 107 | @available(AsyncAlgorithms 1.0, *) 108 | extension AsyncChain3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable {} 109 | 110 | @available(*, unavailable) 111 | extension AsyncChain3Sequence.Iterator: Sendable {} 112 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncChunkedOnProjectionSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | extension AsyncSequence { 14 | /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` type on the uniqueness of a given subject. 15 | @available(AsyncAlgorithms 1.0, *) 16 | @inlinable 17 | public func chunked( 18 | into: Collected.Type, 19 | on projection: @escaping @Sendable (Element) -> Subject 20 | ) -> AsyncChunkedOnProjectionSequence { 21 | AsyncChunkedOnProjectionSequence(self, projection: projection) 22 | } 23 | 24 | /// Creates an asynchronous sequence that creates chunks on the uniqueness of a given subject. 25 | @available(AsyncAlgorithms 1.0, *) 26 | @inlinable 27 | public func chunked( 28 | on projection: @escaping @Sendable (Element) -> Subject 29 | ) -> AsyncChunkedOnProjectionSequence { 30 | chunked(into: [Element].self, on: projection) 31 | } 32 | } 33 | 34 | /// An `AsyncSequence` that chunks on a subject when it differs from the last element. 35 | @available(AsyncAlgorithms 1.0, *) 36 | public struct AsyncChunkedOnProjectionSequence< 37 | Base: AsyncSequence, 38 | Subject: Equatable, 39 | Collected: RangeReplaceableCollection 40 | >: AsyncSequence where Collected.Element == Base.Element { 41 | public typealias Element = (Subject, Collected) 42 | 43 | /// The iterator for a `AsyncChunkedOnProjectionSequence` instance. 44 | @frozen 45 | public struct Iterator: AsyncIteratorProtocol { 46 | 47 | @usableFromInline 48 | var base: Base.AsyncIterator 49 | 50 | @usableFromInline 51 | let projection: @Sendable (Base.Element) -> Subject 52 | 53 | @usableFromInline 54 | init(base: Base.AsyncIterator, projection: @escaping @Sendable (Base.Element) -> Subject) { 55 | self.base = base 56 | self.projection = projection 57 | } 58 | 59 | @usableFromInline 60 | var hangingNext: (Subject, Base.Element)? 61 | 62 | @inlinable 63 | public mutating func next() async rethrows -> (Subject, Collected)? { 64 | var firstOpt = hangingNext 65 | if firstOpt == nil { 66 | let nextOpt = try await base.next() 67 | if let next = nextOpt { 68 | firstOpt = (projection(next), next) 69 | } 70 | } else { 71 | hangingNext = nil 72 | } 73 | 74 | guard let first = firstOpt else { 75 | return nil 76 | } 77 | 78 | var result: Collected = .init() 79 | result.append(first.1) 80 | 81 | while let next = try await base.next() { 82 | let subj = projection(next) 83 | guard subj == first.0 else { 84 | hangingNext = (subj, next) 85 | break 86 | } 87 | result.append(next) 88 | } 89 | return (first.0, result) 90 | } 91 | } 92 | 93 | @usableFromInline 94 | let base: Base 95 | 96 | @usableFromInline 97 | let projection: @Sendable (Base.Element) -> Subject 98 | 99 | @usableFromInline 100 | init(_ base: Base, projection: @escaping @Sendable (Base.Element) -> Subject) { 101 | self.base = base 102 | self.projection = projection 103 | } 104 | 105 | @inlinable 106 | public func makeAsyncIterator() -> Iterator { 107 | Iterator(base: base.makeAsyncIterator(), projection: projection) 108 | } 109 | } 110 | 111 | @available(AsyncAlgorithms 1.0, *) 112 | extension AsyncChunkedOnProjectionSequence: Sendable where Base: Sendable, Base.Element: Sendable {} 113 | 114 | @available(*, unavailable) 115 | extension AsyncChunkedOnProjectionSequence.Iterator: Sendable {} 116 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncChunksOfCountSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2021 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | extension AsyncSequence { 14 | /// Creates an asynchronous sequence that creates chunks of a given `RangeReplaceableCollection` of a given count. 15 | @available(AsyncAlgorithms 1.0, *) 16 | @inlinable 17 | public func chunks( 18 | ofCount count: Int, 19 | into: Collected.Type 20 | ) -> AsyncChunksOfCountSequence where Collected.Element == Element { 21 | AsyncChunksOfCountSequence(self, count: count) 22 | } 23 | 24 | /// Creates an asynchronous sequence that creates chunks of a given count. 25 | @available(AsyncAlgorithms 1.0, *) 26 | @inlinable 27 | public func chunks(ofCount count: Int) -> AsyncChunksOfCountSequence { 28 | chunks(ofCount: count, into: [Element].self) 29 | } 30 | } 31 | 32 | /// An `AsyncSequence` that chunks elements into `RangeReplaceableCollection` instances of at least a given count. 33 | @available(AsyncAlgorithms 1.0, *) 34 | public struct AsyncChunksOfCountSequence: AsyncSequence 35 | where Collected.Element == Base.Element { 36 | public typealias Element = Collected 37 | 38 | /// The iterator for a `AsyncChunksOfCountSequence` instance. 39 | @frozen 40 | public struct Iterator: AsyncIteratorProtocol { 41 | 42 | @usableFromInline 43 | var base: Base.AsyncIterator 44 | 45 | @usableFromInline 46 | let count: Int 47 | 48 | @usableFromInline 49 | init(base: Base.AsyncIterator, count: Int) { 50 | self.base = base 51 | self.count = count 52 | } 53 | 54 | @inlinable 55 | public mutating func next() async rethrows -> Collected? { 56 | guard let first = try await base.next() else { 57 | return nil 58 | } 59 | 60 | if count == 1 { 61 | return Collected(CollectionOfOne(first)) 62 | } 63 | 64 | var result: Collected = .init() 65 | result.append(first) 66 | 67 | while let next = try await base.next() { 68 | result.append(next) 69 | if result.count == count { 70 | break 71 | } 72 | } 73 | return result 74 | } 75 | } 76 | 77 | @usableFromInline 78 | let base: Base 79 | 80 | @usableFromInline 81 | let count: Int 82 | 83 | @usableFromInline 84 | init(_ base: Base, count: Int) { 85 | precondition(count > 0) 86 | self.base = base 87 | self.count = count 88 | } 89 | 90 | @inlinable 91 | public func makeAsyncIterator() -> Iterator { 92 | Iterator(base: base.makeAsyncIterator(), count: count) 93 | } 94 | } 95 | 96 | @available(AsyncAlgorithms 1.0, *) 97 | extension AsyncChunksOfCountSequence: Sendable where Base: Sendable, Base.Element: Sendable {} 98 | @available(AsyncAlgorithms 1.0, *) 99 | extension AsyncChunksOfCountSequence.Iterator: Sendable where Base.AsyncIterator: Sendable, Base.Element: Sendable {} 100 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncCompactedSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | extension AsyncSequence { 14 | /// Returns a new `AsyncSequence` that iterates over every non-nil element from the 15 | /// original `AsyncSequence`. 16 | /// 17 | /// Produces the same result as `c.compactMap { $0 }`. 18 | /// 19 | /// - Returns: An `AsyncSequence` where the element is the unwrapped original 20 | /// element and iterates over every non-nil element from the original 21 | /// `AsyncSequence`. 22 | @available(AsyncAlgorithms 1.0, *) 23 | @inlinable 24 | public func compacted() -> AsyncCompactedSequence 25 | where Element == Unwrapped? { 26 | AsyncCompactedSequence(self) 27 | } 28 | } 29 | 30 | /// An `AsyncSequence` that iterates over every non-nil element from the original 31 | /// `AsyncSequence`. 32 | @available(AsyncAlgorithms 1.0, *) 33 | @frozen 34 | public struct AsyncCompactedSequence: AsyncSequence 35 | where Base.Element == Element? { 36 | 37 | @usableFromInline 38 | let base: Base 39 | 40 | @inlinable 41 | init(_ base: Base) { 42 | self.base = base 43 | } 44 | 45 | /// The iterator for an `AsyncCompactedSequence` instance. 46 | @frozen 47 | public struct Iterator: AsyncIteratorProtocol { 48 | @usableFromInline 49 | var base: Base.AsyncIterator 50 | 51 | @inlinable 52 | init(_ base: Base.AsyncIterator) { 53 | self.base = base 54 | } 55 | 56 | @inlinable 57 | public mutating func next() async rethrows -> Element? { 58 | while let wrapped = try await base.next() { 59 | guard let some = wrapped else { continue } 60 | return some 61 | } 62 | return nil 63 | } 64 | } 65 | 66 | @inlinable 67 | public func makeAsyncIterator() -> Iterator { 68 | Iterator(base.makeAsyncIterator()) 69 | } 70 | } 71 | 72 | @available(AsyncAlgorithms 1.0, *) 73 | extension AsyncCompactedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} 74 | 75 | @available(*, unavailable) 76 | extension AsyncCompactedSequence.Iterator: Sendable {} 77 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncInclusiveReductionsSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | extension AsyncSequence { 14 | /// Returns an asynchronous sequence containing the accumulated results of combining the 15 | /// elements of the asynchronous sequence using the given closure. 16 | /// 17 | /// This can be seen as applying the reduce function to each element and 18 | /// producing an asynchronous sequence consisting of the initial value followed 19 | /// by these results. 20 | /// 21 | /// ``` 22 | /// let runningTotal = [1, 2, 3, 4].async.reductions(+) 23 | /// print(await Array(runningTotal)) 24 | /// 25 | /// // prints [1, 3, 6, 10] 26 | /// ``` 27 | /// 28 | /// - Parameter transform: A closure that combines the previously reduced 29 | /// result and the next element in the receiving sequence, and returns 30 | /// the result. 31 | /// - Returns: An asynchronous sequence of the reduced elements. 32 | @available(AsyncAlgorithms 1.0, *) 33 | @inlinable 34 | public func reductions( 35 | _ transform: @Sendable @escaping (Element, Element) async -> Element 36 | ) -> AsyncInclusiveReductionsSequence { 37 | AsyncInclusiveReductionsSequence(self, transform: transform) 38 | } 39 | } 40 | 41 | /// An asynchronous sequence containing the accumulated results of combining the 42 | /// elements of the asynchronous sequence using a given closure. 43 | @available(AsyncAlgorithms 1.0, *) 44 | @frozen 45 | public struct AsyncInclusiveReductionsSequence { 46 | @usableFromInline 47 | let base: Base 48 | 49 | @usableFromInline 50 | let transform: @Sendable (Base.Element, Base.Element) async -> Base.Element 51 | 52 | @inlinable 53 | init(_ base: Base, transform: @Sendable @escaping (Base.Element, Base.Element) async -> Base.Element) { 54 | self.base = base 55 | self.transform = transform 56 | } 57 | } 58 | 59 | @available(AsyncAlgorithms 1.0, *) 60 | extension AsyncInclusiveReductionsSequence: AsyncSequence { 61 | public typealias Element = Base.Element 62 | 63 | /// The iterator for an `AsyncInclusiveReductionsSequence` instance. 64 | @available(AsyncAlgorithms 1.0, *) 65 | @frozen 66 | public struct Iterator: AsyncIteratorProtocol { 67 | @usableFromInline 68 | internal var iterator: Base.AsyncIterator 69 | 70 | @usableFromInline 71 | internal var element: Base.Element? 72 | 73 | @usableFromInline 74 | internal let transform: @Sendable (Base.Element, Base.Element) async -> Base.Element 75 | 76 | @inlinable 77 | init( 78 | _ iterator: Base.AsyncIterator, 79 | transform: @Sendable @escaping (Base.Element, Base.Element) async -> Base.Element 80 | ) { 81 | self.iterator = iterator 82 | self.transform = transform 83 | } 84 | 85 | @inlinable 86 | public mutating func next() async rethrows -> Base.Element? { 87 | guard let previous = element else { 88 | element = try await iterator.next() 89 | return element 90 | } 91 | guard let next = try await iterator.next() else { return nil } 92 | element = await transform(previous, next) 93 | return element 94 | } 95 | } 96 | 97 | @available(AsyncAlgorithms 1.0, *) 98 | @inlinable 99 | public func makeAsyncIterator() -> Iterator { 100 | Iterator(base.makeAsyncIterator(), transform: transform) 101 | } 102 | } 103 | 104 | @available(AsyncAlgorithms 1.0, *) 105 | extension AsyncInclusiveReductionsSequence: Sendable where Base: Sendable {} 106 | 107 | @available(*, unavailable) 108 | extension AsyncInclusiveReductionsSequence.Iterator: Sendable {} 109 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncJoinedSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | extension AsyncSequence where Element: AsyncSequence { 14 | /// Concatenate an `AsyncSequence` of `AsyncSequence` elements 15 | @available(AsyncAlgorithms 1.0, *) 16 | @inlinable 17 | public func joined() -> AsyncJoinedSequence { 18 | return AsyncJoinedSequence(self) 19 | } 20 | } 21 | 22 | /// An `AsyncSequence` that concatenates`AsyncSequence` elements 23 | @available(AsyncAlgorithms 1.0, *) 24 | @frozen 25 | public struct AsyncJoinedSequence: AsyncSequence where Base.Element: AsyncSequence { 26 | public typealias Element = Base.Element.Element 27 | public typealias AsyncIterator = Iterator 28 | 29 | /// The iterator for an `AsyncJoinedSequence` instance. 30 | @frozen 31 | public struct Iterator: AsyncIteratorProtocol { 32 | @usableFromInline 33 | enum State { 34 | case initial(Base.AsyncIterator) 35 | case sequence(Base.AsyncIterator, Base.Element.AsyncIterator) 36 | case terminal 37 | } 38 | 39 | @usableFromInline 40 | var state: State 41 | 42 | @inlinable 43 | init(_ iterator: Base.AsyncIterator) { 44 | state = .initial(iterator) 45 | } 46 | 47 | @inlinable 48 | public mutating func next() async rethrows -> Base.Element.Element? { 49 | do { 50 | switch state { 51 | case .terminal: 52 | return nil 53 | case .initial(var outerIterator): 54 | guard let innerSequence = try await outerIterator.next() else { 55 | state = .terminal 56 | return nil 57 | } 58 | let innerIterator = innerSequence.makeAsyncIterator() 59 | state = .sequence(outerIterator, innerIterator) 60 | return try await next() 61 | case .sequence(var outerIterator, var innerIterator): 62 | if let item = try await innerIterator.next() { 63 | state = .sequence(outerIterator, innerIterator) 64 | return item 65 | } 66 | 67 | guard let nextInner = try await outerIterator.next() else { 68 | state = .terminal 69 | return nil 70 | } 71 | 72 | state = .sequence(outerIterator, nextInner.makeAsyncIterator()) 73 | return try await next() 74 | } 75 | } catch { 76 | state = .terminal 77 | throw error 78 | } 79 | } 80 | } 81 | 82 | @usableFromInline 83 | let base: Base 84 | 85 | @usableFromInline 86 | init(_ base: Base) { 87 | self.base = base 88 | } 89 | 90 | @inlinable 91 | public func makeAsyncIterator() -> Iterator { 92 | return Iterator(base.makeAsyncIterator()) 93 | } 94 | } 95 | 96 | @available(AsyncAlgorithms 1.0, *) 97 | extension AsyncJoinedSequence: Sendable 98 | where Base: Sendable, Base.Element: Sendable, Base.Element.Element: Sendable {} 99 | 100 | @available(*, unavailable) 101 | extension AsyncJoinedSequence.Iterator: Sendable {} 102 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncSyncSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | extension Sequence { 14 | /// An asynchronous sequence containing the same elements as this sequence, 15 | /// but on which operations, such as `map` and `filter`, are 16 | /// implemented asynchronously. 17 | @available(AsyncAlgorithms 1.0, *) 18 | @inlinable 19 | public var async: AsyncSyncSequence { 20 | AsyncSyncSequence(self) 21 | } 22 | } 23 | 24 | /// An asynchronous sequence composed from a synchronous sequence. 25 | /// 26 | /// Asynchronous lazy sequences can be used to interface existing or pre-calculated 27 | /// data to interoperate with other asynchronous sequences and algorithms based on 28 | /// asynchronous sequences. 29 | /// 30 | /// This functions similarly to `LazySequence` by accessing elements sequentially 31 | /// in the iterator's `next()` method. 32 | @available(AsyncAlgorithms 1.0, *) 33 | @frozen 34 | public struct AsyncSyncSequence: AsyncSequence { 35 | public typealias Element = Base.Element 36 | 37 | @frozen 38 | public struct Iterator: AsyncIteratorProtocol { 39 | @usableFromInline 40 | var iterator: Base.Iterator? 41 | 42 | @usableFromInline 43 | init(_ iterator: Base.Iterator) { 44 | self.iterator = iterator 45 | } 46 | 47 | @inlinable 48 | public mutating func next() async -> Base.Element? { 49 | guard !Task.isCancelled, let value = iterator?.next() else { 50 | iterator = nil 51 | return nil 52 | } 53 | return value 54 | } 55 | } 56 | 57 | @usableFromInline 58 | let base: Base 59 | 60 | @usableFromInline 61 | init(_ base: Base) { 62 | self.base = base 63 | } 64 | 65 | @inlinable 66 | public func makeAsyncIterator() -> Iterator { 67 | Iterator(base.makeIterator()) 68 | } 69 | } 70 | 71 | @available(AsyncAlgorithms 1.0, *) 72 | extension AsyncSyncSequence: Sendable where Base: Sendable {} 73 | 74 | @available(*, unavailable) 75 | extension AsyncSyncSequence.Iterator: Sendable {} 76 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncThrowingInclusiveReductionsSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | extension AsyncSequence { 14 | /// Returns an asynchronous sequence containing the accumulated results of combining the 15 | /// elements of the asynchronous sequence using the given error-throwing closure. 16 | /// 17 | /// This can be seen as applying the reduce function to each element and 18 | /// providing the initial value followed by these results as an asynchronous sequence. 19 | /// 20 | /// ``` 21 | /// let runningTotal = [1, 2, 3, 4].async.reductions(+) 22 | /// print(await Array(runningTotal)) 23 | /// 24 | /// // prints [1, 3, 6, 10] 25 | /// ``` 26 | /// 27 | /// - Parameter transform: A closure that combines the previously reduced 28 | /// result and the next element in the receiving sequence. If the closure 29 | /// throws an error, the sequence throws. 30 | /// - Returns: An asynchronous sequence of the reduced elements. 31 | @available(AsyncAlgorithms 1.0, *) 32 | @inlinable 33 | public func reductions( 34 | _ transform: @Sendable @escaping (Element, Element) async throws -> Element 35 | ) -> AsyncThrowingInclusiveReductionsSequence { 36 | AsyncThrowingInclusiveReductionsSequence(self, transform: transform) 37 | } 38 | } 39 | 40 | /// An asynchronous sequence containing the accumulated results of combining the 41 | /// elements of the asynchronous sequence using a given error-throwing closure. 42 | @available(AsyncAlgorithms 1.0, *) 43 | @frozen 44 | public struct AsyncThrowingInclusiveReductionsSequence { 45 | @usableFromInline 46 | let base: Base 47 | 48 | @usableFromInline 49 | let transform: @Sendable (Base.Element, Base.Element) async throws -> Base.Element 50 | 51 | @inlinable 52 | init(_ base: Base, transform: @Sendable @escaping (Base.Element, Base.Element) async throws -> Base.Element) { 53 | self.base = base 54 | self.transform = transform 55 | } 56 | } 57 | 58 | @available(AsyncAlgorithms 1.0, *) 59 | extension AsyncThrowingInclusiveReductionsSequence: AsyncSequence { 60 | public typealias Element = Base.Element 61 | 62 | /// The iterator for an `AsyncThrowingInclusiveReductionsSequence` instance. 63 | @available(AsyncAlgorithms 1.0, *) 64 | @frozen 65 | public struct Iterator: AsyncIteratorProtocol { 66 | @usableFromInline 67 | internal var iterator: Base.AsyncIterator? 68 | 69 | @usableFromInline 70 | internal var element: Base.Element? 71 | 72 | @usableFromInline 73 | internal let transform: @Sendable (Base.Element, Base.Element) async throws -> Base.Element 74 | 75 | @inlinable 76 | init( 77 | _ iterator: Base.AsyncIterator, 78 | transform: @Sendable @escaping (Base.Element, Base.Element) async throws -> Base.Element 79 | ) { 80 | self.iterator = iterator 81 | self.transform = transform 82 | } 83 | 84 | @inlinable 85 | public mutating func next() async throws -> Base.Element? { 86 | guard let previous = element else { 87 | element = try await iterator?.next() 88 | return element 89 | } 90 | guard let next = try await iterator?.next() else { return nil } 91 | do { 92 | element = try await transform(previous, next) 93 | } catch { 94 | iterator = nil 95 | throw error 96 | } 97 | return element 98 | } 99 | } 100 | 101 | @available(AsyncAlgorithms 1.0, *) 102 | @inlinable 103 | public func makeAsyncIterator() -> Iterator { 104 | Iterator(base.makeAsyncIterator(), transform: transform) 105 | } 106 | } 107 | 108 | @available(AsyncAlgorithms 1.0, *) 109 | extension AsyncThrowingInclusiveReductionsSequence: Sendable where Base: Sendable {} 110 | 111 | @available(*, unavailable) 112 | extension AsyncThrowingInclusiveReductionsSequence.Iterator: Sendable {} 113 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/AsyncTimerSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | /// An `AsyncSequence` that produces elements at regular intervals. 13 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 14 | public struct AsyncTimerSequence: AsyncSequence { 15 | public typealias Element = C.Instant 16 | 17 | /// The iterator for an `AsyncTimerSequence` instance. 18 | public struct Iterator: AsyncIteratorProtocol { 19 | var clock: C? 20 | let interval: C.Instant.Duration 21 | let tolerance: C.Instant.Duration? 22 | var last: C.Instant? 23 | 24 | init(interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { 25 | self.clock = clock 26 | self.interval = interval 27 | self.tolerance = tolerance 28 | } 29 | 30 | public mutating func next() async -> C.Instant? { 31 | guard let clock = self.clock else { 32 | return nil 33 | } 34 | 35 | let next = (self.last ?? clock.now).advanced(by: self.interval) 36 | do { 37 | try await clock.sleep(until: next, tolerance: self.tolerance) 38 | } catch { 39 | self.clock = nil 40 | return nil 41 | } 42 | let now = clock.now 43 | self.last = next 44 | return now 45 | } 46 | } 47 | 48 | let clock: C 49 | let interval: C.Instant.Duration 50 | let tolerance: C.Instant.Duration? 51 | 52 | /// Create an `AsyncTimerSequence` with a given repeating interval. 53 | public init(interval: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C) { 54 | self.clock = clock 55 | self.interval = interval 56 | self.tolerance = tolerance 57 | } 58 | 59 | public func makeAsyncIterator() -> Iterator { 60 | Iterator(interval: interval, tolerance: tolerance, clock: clock) 61 | } 62 | } 63 | 64 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 65 | extension AsyncTimerSequence { 66 | /// Create an `AsyncTimerSequence` with a given repeating interval. 67 | public static func repeating( 68 | every interval: C.Instant.Duration, 69 | tolerance: C.Instant.Duration? = nil, 70 | clock: C 71 | ) -> AsyncTimerSequence { 72 | return AsyncTimerSequence(interval: interval, tolerance: tolerance, clock: clock) 73 | } 74 | } 75 | 76 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 77 | extension AsyncTimerSequence where C == SuspendingClock { 78 | /// Create an `AsyncTimerSequence` with a given repeating interval. 79 | public static func repeating( 80 | every interval: Duration, 81 | tolerance: Duration? = nil 82 | ) -> AsyncTimerSequence { 83 | return AsyncTimerSequence(interval: interval, tolerance: tolerance, clock: SuspendingClock()) 84 | } 85 | } 86 | 87 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 88 | extension AsyncTimerSequence: Sendable {} 89 | 90 | @available(*, unavailable) 91 | extension AsyncTimerSequence.Iterator: Sendable {} 92 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | final class UnboundedBufferStorage: Sendable where Base: Sendable { 14 | private let stateMachine: ManagedCriticalState> 15 | 16 | init(base: Base, policy: UnboundedBufferStateMachine.Policy) { 17 | self.stateMachine = ManagedCriticalState(UnboundedBufferStateMachine(base: base, policy: policy)) 18 | } 19 | 20 | func next() async -> Result? { 21 | return await withTaskCancellationHandler { 22 | 23 | let action: UnboundedBufferStateMachine.NextAction? = self.stateMachine.withCriticalRegion { 24 | stateMachine in 25 | let action = stateMachine.next() 26 | switch action { 27 | case .startTask(let base): 28 | self.startTask(stateMachine: &stateMachine, base: base) 29 | return nil 30 | case .suspend: 31 | return action 32 | case .returnResult: 33 | return action 34 | } 35 | } 36 | 37 | switch action { 38 | case .startTask: 39 | // We are handling the startTask in the lock already because we want to avoid 40 | // other inputs interleaving while starting the task 41 | fatalError("Internal inconsistency") 42 | case .suspend: 43 | break 44 | case .returnResult(let result): 45 | return result 46 | case .none: 47 | break 48 | } 49 | 50 | return await withUnsafeContinuation { 51 | (continuation: UnsafeContinuation?, Never>) in 52 | let action = self.stateMachine.withCriticalRegion { stateMachine in 53 | stateMachine.nextSuspended(continuation: continuation) 54 | } 55 | switch action { 56 | case .none: 57 | break 58 | case .resumeConsumer(let result): 59 | continuation.resume(returning: result) 60 | } 61 | } 62 | } onCancel: { 63 | self.interrupted() 64 | } 65 | } 66 | 67 | private func startTask( 68 | stateMachine: inout UnboundedBufferStateMachine, 69 | base: Base 70 | ) { 71 | let task = Task { 72 | do { 73 | for try await element in base { 74 | let action = self.stateMachine.withCriticalRegion { stateMachine in 75 | stateMachine.elementProduced(element: element) 76 | } 77 | switch action { 78 | case .none: 79 | break 80 | case .resumeConsumer(let continuation, let result): 81 | continuation.resume(returning: result) 82 | } 83 | } 84 | 85 | let action = self.stateMachine.withCriticalRegion { stateMachine in 86 | stateMachine.finish(error: nil) 87 | } 88 | switch action { 89 | case .none: 90 | break 91 | case .resumeConsumer(let continuation): 92 | continuation?.resume(returning: nil) 93 | } 94 | } catch { 95 | let action = self.stateMachine.withCriticalRegion { stateMachine in 96 | stateMachine.finish(error: error) 97 | } 98 | switch action { 99 | case .none: 100 | break 101 | case .resumeConsumer(let continuation): 102 | continuation?.resume(returning: .failure(error)) 103 | } 104 | } 105 | } 106 | 107 | stateMachine.taskStarted(task: task) 108 | } 109 | 110 | func interrupted() { 111 | let action = self.stateMachine.withCriticalRegion { stateMachine in 112 | stateMachine.interrupted() 113 | } 114 | switch action { 115 | case .none: 116 | break 117 | case .resumeConsumer(let task, let continuation): 118 | task.cancel() 119 | continuation?.resume(returning: nil) 120 | } 121 | } 122 | 123 | deinit { 124 | self.interrupted() 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/Channels/AsyncChannel.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | /// A channel for sending elements from one task to another with back pressure. 13 | /// 14 | /// The `AsyncChannel` class is intended to be used as a communication type between tasks, 15 | /// particularly when one task produces values and another task consumes those values. The back 16 | /// pressure applied by `send(_:)` via the suspension/resume ensures that 17 | /// the production of values does not exceed the consumption of values from iteration. This method 18 | /// suspends after enqueuing the event and is resumed when the next call to `next()` 19 | /// on the `Iterator` is made, or when `finish()` is called from another Task. 20 | /// As `finish()` induces a terminal state, there is no more need for a back pressure management. 21 | /// This function does not suspend and will finish all the pending iterations. 22 | @available(AsyncAlgorithms 1.0, *) 23 | public final class AsyncChannel: AsyncSequence, Sendable { 24 | public typealias Element = Element 25 | public typealias AsyncIterator = Iterator 26 | 27 | let storage: ChannelStorage 28 | 29 | public init() { 30 | self.storage = ChannelStorage() 31 | } 32 | 33 | /// Sends an element to an awaiting iteration. This function will resume when the next call to `next()` is made 34 | /// or when a call to `finish()` is made from another task. 35 | /// If the channel is already finished then this returns immediately. 36 | /// If the task is cancelled, this function will resume without sending the element. 37 | /// Other sending operations from other tasks will remain active. 38 | public func send(_ element: Element) async { 39 | await self.storage.send(element: element) 40 | } 41 | 42 | /// Immediately resumes all the suspended operations. 43 | /// All subsequent calls to `next(_:)` will resume immediately. 44 | public func finish() { 45 | self.storage.finish() 46 | } 47 | 48 | public func makeAsyncIterator() -> Iterator { 49 | Iterator(storage: self.storage) 50 | } 51 | 52 | public struct Iterator: AsyncIteratorProtocol { 53 | let storage: ChannelStorage 54 | 55 | public mutating func next() async -> Element? { 56 | // Although the storage can throw, its usage in the context of an `AsyncChannel` guarantees it cannot. 57 | // There is no public way of sending a failure to it. 58 | try! await self.storage.next() 59 | } 60 | } 61 | } 62 | 63 | @available(*, unavailable) 64 | extension AsyncChannel.Iterator: Sendable {} 65 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/Channels/AsyncThrowingChannel.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | /// An error-throwing channel for sending elements from on task to another with back pressure. 13 | /// 14 | /// The `AsyncThrowingChannel` class is intended to be used as a communication types between tasks, 15 | /// particularly when one task produces values and another task consumes those values. The back 16 | /// pressure applied by `send(_:)` via suspension/resume ensures that the production of values does 17 | /// not exceed the consumption of values from iteration. This method suspends after enqueuing the event 18 | /// and is resumed when the next call to `next()` on the `Iterator` is made, or when `finish()`/`fail(_:)` is called 19 | /// from another Task. As `finish()` and `fail(_:)` induce a terminal state, there is no more need for a back pressure management. 20 | /// Those functions do not suspend and will finish all the pending iterations. 21 | @available(AsyncAlgorithms 1.0, *) 22 | public final class AsyncThrowingChannel: AsyncSequence, Sendable { 23 | public typealias Element = Element 24 | public typealias AsyncIterator = Iterator 25 | 26 | let storage: ChannelStorage 27 | 28 | public init() { 29 | self.storage = ChannelStorage() 30 | } 31 | 32 | /// Sends an element to an awaiting iteration. This function will resume when the next call to `next()` is made 33 | /// or when a call to `finish()` or `fail` is made from another task. 34 | /// If the channel is already finished then this returns immediately. 35 | /// If the task is cancelled, this function will resume without sending the element. 36 | /// Other sending operations from other tasks will remain active. 37 | public func send(_ element: Element) async { 38 | await self.storage.send(element: element) 39 | } 40 | 41 | /// Sends an error to all awaiting iterations. 42 | /// All subsequent calls to `next(_:)` will resume immediately. 43 | public func fail(_ error: Error) where Failure == Error { 44 | self.storage.finish(error: error) 45 | } 46 | 47 | /// Immediately resumes all the suspended operations. 48 | /// All subsequent calls to `next(_:)` will resume immediately. 49 | public func finish() { 50 | self.storage.finish() 51 | } 52 | 53 | public func makeAsyncIterator() -> Iterator { 54 | Iterator(storage: self.storage) 55 | } 56 | 57 | public struct Iterator: AsyncIteratorProtocol { 58 | let storage: ChannelStorage 59 | 60 | public mutating func next() async throws -> Element? { 61 | try await self.storage.next() 62 | } 63 | } 64 | } 65 | 66 | @available(*, unavailable) 67 | extension AsyncThrowingChannel.Iterator: Sendable {} 68 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/Channels/ChannelStorage.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | @available(AsyncAlgorithms 1.0, *) 12 | struct ChannelStorage: Sendable { 13 | private let stateMachine: ManagedCriticalState> 14 | private let ids = ManagedCriticalState(0) 15 | 16 | init() { 17 | self.stateMachine = ManagedCriticalState(ChannelStateMachine()) 18 | } 19 | 20 | func generateId() -> UInt64 { 21 | self.ids.withCriticalRegion { ids in 22 | defer { ids &+= 1 } 23 | return ids 24 | } 25 | } 26 | 27 | func send(element: Element) async { 28 | // check if a suspension is needed 29 | let action = self.stateMachine.withCriticalRegion { stateMachine in 30 | stateMachine.send() 31 | } 32 | 33 | switch action { 34 | case .suspend: 35 | break 36 | 37 | case .resumeConsumer(let continuation): 38 | continuation?.resume(returning: element) 39 | return 40 | } 41 | 42 | let producerID = self.generateId() 43 | 44 | await withTaskCancellationHandler { 45 | // a suspension is needed 46 | await withUnsafeContinuation { (continuation: UnsafeContinuation) in 47 | let action = self.stateMachine.withCriticalRegion { stateMachine in 48 | stateMachine.sendSuspended(continuation: continuation, element: element, producerID: producerID) 49 | } 50 | 51 | switch action { 52 | case .none: 53 | break 54 | case .resumeProducer: 55 | continuation.resume() 56 | case .resumeProducerAndConsumer(let consumerContinuation): 57 | continuation.resume() 58 | consumerContinuation?.resume(returning: element) 59 | } 60 | } 61 | } onCancel: { 62 | let action = self.stateMachine.withCriticalRegion { stateMachine in 63 | stateMachine.sendCancelled(producerID: producerID) 64 | } 65 | 66 | switch action { 67 | case .none: 68 | break 69 | case .resumeProducer(let continuation): 70 | continuation?.resume() 71 | } 72 | } 73 | } 74 | 75 | func finish(error: Failure? = nil) { 76 | let action = self.stateMachine.withCriticalRegion { stateMachine in 77 | stateMachine.finish(error: error) 78 | } 79 | 80 | switch action { 81 | case .none: 82 | break 83 | case .resumeProducersAndConsumers(let producerContinuations, let consumerContinuations): 84 | producerContinuations.forEach { $0?.resume() } 85 | if let error { 86 | consumerContinuations.forEach { $0?.resume(throwing: error) } 87 | } else { 88 | consumerContinuations.forEach { $0?.resume(returning: nil) } 89 | } 90 | } 91 | } 92 | 93 | func next() async throws -> Element? { 94 | let action = self.stateMachine.withCriticalRegion { stateMachine in 95 | stateMachine.next() 96 | } 97 | 98 | switch action { 99 | case .suspend: 100 | break 101 | 102 | case .resumeProducer(let producerContinuation, let result): 103 | producerContinuation?.resume() 104 | return try result._rethrowGet() 105 | } 106 | 107 | let consumerID = self.generateId() 108 | 109 | return try await withTaskCancellationHandler { 110 | try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in 111 | let action = self.stateMachine.withCriticalRegion { stateMachine in 112 | stateMachine.nextSuspended( 113 | continuation: continuation, 114 | consumerID: consumerID 115 | ) 116 | } 117 | 118 | switch action { 119 | case .none: 120 | break 121 | case .resumeConsumer(let element): 122 | continuation.resume(returning: element) 123 | case .resumeConsumerWithError(let error): 124 | continuation.resume(throwing: error) 125 | case .resumeProducerAndConsumer(let producerContinuation, let element): 126 | producerContinuation?.resume() 127 | continuation.resume(returning: element) 128 | } 129 | } 130 | } onCancel: { 131 | let action = self.stateMachine.withCriticalRegion { stateMachine in 132 | stateMachine.nextCancelled(consumerID: consumerID) 133 | } 134 | 135 | switch action { 136 | case .none: 137 | break 138 | case .resumeConsumer(let continuation): 139 | continuation?.resume(returning: nil) 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | /// Creates an asynchronous sequence that combines the latest values from two `AsyncSequence` types 13 | /// by emitting a tuple of the values. ``combineLatest(_:_:)`` only emits a value whenever any of the base `AsyncSequence`s 14 | /// emit a value (so long as each of the bases have emitted at least one value). 15 | /// 16 | /// Finishes: 17 | /// ``combineLatest(_:_:)`` finishes when one of the bases finishes before emitting any value or 18 | /// when all bases finished. 19 | /// 20 | /// Throws: 21 | /// ``combineLatest(_:_:)`` throws when one of the bases throws. If one of the bases threw any buffered and not yet consumed 22 | /// values will be dropped. 23 | @available(AsyncAlgorithms 1.0, *) 24 | public func combineLatest< 25 | Base1: AsyncSequence, 26 | Base2: AsyncSequence 27 | >(_ base1: Base1, _ base2: Base2) -> AsyncCombineLatest2Sequence 28 | where 29 | Base1: Sendable, 30 | Base1.Element: Sendable, 31 | Base2: Sendable, 32 | Base2.Element: Sendable 33 | { 34 | AsyncCombineLatest2Sequence(base1, base2) 35 | } 36 | 37 | /// An `AsyncSequence` that combines the latest values produced from two asynchronous sequences into an asynchronous sequence of tuples. 38 | @available(AsyncAlgorithms 1.0, *) 39 | public struct AsyncCombineLatest2Sequence< 40 | Base1: AsyncSequence, 41 | Base2: AsyncSequence 42 | >: AsyncSequence, Sendable 43 | where 44 | Base1: Sendable, 45 | Base1.Element: Sendable, 46 | Base2: Sendable, 47 | Base2.Element: Sendable 48 | { 49 | public typealias Element = (Base1.Element, Base2.Element) 50 | public typealias AsyncIterator = Iterator 51 | 52 | let base1: Base1 53 | let base2: Base2 54 | 55 | init(_ base1: Base1, _ base2: Base2) { 56 | self.base1 = base1 57 | self.base2 = base2 58 | } 59 | 60 | public func makeAsyncIterator() -> AsyncIterator { 61 | Iterator(storage: .init(self.base1, self.base2, nil)) 62 | } 63 | 64 | public struct Iterator: AsyncIteratorProtocol { 65 | final class InternalClass { 66 | private let storage: CombineLatestStorage 67 | 68 | fileprivate init(storage: CombineLatestStorage) { 69 | self.storage = storage 70 | } 71 | 72 | deinit { 73 | self.storage.iteratorDeinitialized() 74 | } 75 | 76 | func next() async rethrows -> Element? { 77 | guard let element = try await self.storage.next() else { 78 | return nil 79 | } 80 | 81 | return (element.0, element.1) 82 | } 83 | } 84 | 85 | let internalClass: InternalClass 86 | 87 | fileprivate init(storage: CombineLatestStorage) { 88 | self.internalClass = InternalClass(storage: storage) 89 | } 90 | 91 | public mutating func next() async rethrows -> Element? { 92 | try await self.internalClass.next() 93 | } 94 | } 95 | } 96 | 97 | @available(*, unavailable) 98 | extension AsyncCombineLatest2Sequence.Iterator: Sendable {} 99 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | /// Creates an asynchronous sequence that combines the latest values from three `AsyncSequence` types 13 | /// by emitting a tuple of the values. ``combineLatest(_:_:_:)`` only emits a value whenever any of the base `AsyncSequence`s 14 | /// emit a value (so long as each of the bases have emitted at least one value). 15 | /// 16 | /// Finishes: 17 | /// ``combineLatest(_:_:_:)`` finishes when one of the bases finishes before emitting any value or 18 | /// when all bases finished. 19 | /// 20 | /// Throws: 21 | /// ``combineLatest(_:_:_:)`` throws when one of the bases throws. If one of the bases threw any buffered and not yet consumed 22 | /// values will be dropped. 23 | @available(AsyncAlgorithms 1.0, *) 24 | public func combineLatest< 25 | Base1: AsyncSequence, 26 | Base2: AsyncSequence, 27 | Base3: AsyncSequence 28 | >(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncCombineLatest3Sequence 29 | where 30 | Base1: Sendable, 31 | Base1.Element: Sendable, 32 | Base2: Sendable, 33 | Base2.Element: Sendable, 34 | Base3: Sendable, 35 | Base3.Element: Sendable 36 | { 37 | AsyncCombineLatest3Sequence(base1, base2, base3) 38 | } 39 | 40 | /// An `AsyncSequence` that combines the latest values produced from three asynchronous sequences into an asynchronous sequence of tuples. 41 | @available(AsyncAlgorithms 1.0, *) 42 | public struct AsyncCombineLatest3Sequence< 43 | Base1: AsyncSequence, 44 | Base2: AsyncSequence, 45 | Base3: AsyncSequence 46 | >: AsyncSequence, Sendable 47 | where 48 | Base1: Sendable, 49 | Base1.Element: Sendable, 50 | Base2: Sendable, 51 | Base2.Element: Sendable, 52 | Base3: Sendable, 53 | Base3.Element: Sendable 54 | { 55 | public typealias Element = (Base1.Element, Base2.Element, Base3.Element) 56 | public typealias AsyncIterator = Iterator 57 | 58 | let base1: Base1 59 | let base2: Base2 60 | let base3: Base3 61 | 62 | init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { 63 | self.base1 = base1 64 | self.base2 = base2 65 | self.base3 = base3 66 | } 67 | 68 | public func makeAsyncIterator() -> AsyncIterator { 69 | Iterator( 70 | storage: .init(self.base1, self.base2, self.base3) 71 | ) 72 | } 73 | 74 | public struct Iterator: AsyncIteratorProtocol { 75 | final class InternalClass { 76 | private let storage: CombineLatestStorage 77 | 78 | fileprivate init(storage: CombineLatestStorage) { 79 | self.storage = storage 80 | } 81 | 82 | deinit { 83 | self.storage.iteratorDeinitialized() 84 | } 85 | 86 | func next() async rethrows -> Element? { 87 | guard let element = try await self.storage.next() else { 88 | return nil 89 | } 90 | 91 | // This force unwrap is safe since there must be a third element. 92 | return (element.0, element.1, element.2!) 93 | } 94 | } 95 | 96 | let internalClass: InternalClass 97 | 98 | fileprivate init(storage: CombineLatestStorage) { 99 | self.internalClass = InternalClass(storage: storage) 100 | } 101 | 102 | public mutating func next() async rethrows -> Element? { 103 | try await self.internalClass.next() 104 | } 105 | } 106 | } 107 | 108 | @available(*, unavailable) 109 | extension AsyncCombineLatest3Sequence.Iterator: Sendable {} 110 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/Debounce/AsyncDebounceSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | extension AsyncSequence { 14 | /// Creates an asynchronous sequence that emits the latest element after a given quiescence period 15 | /// has elapsed by using a specified Clock. 16 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 17 | public func debounce( 18 | for interval: C.Instant.Duration, 19 | tolerance: C.Instant.Duration? = nil, 20 | clock: C 21 | ) -> AsyncDebounceSequence where Self: Sendable, Self.Element: Sendable { 22 | AsyncDebounceSequence(self, interval: interval, tolerance: tolerance, clock: clock) 23 | } 24 | 25 | /// Creates an asynchronous sequence that emits the latest element after a given quiescence period 26 | /// has elapsed. 27 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 28 | public func debounce( 29 | for interval: Duration, 30 | tolerance: Duration? = nil 31 | ) -> AsyncDebounceSequence where Self: Sendable, Self.Element: Sendable { 32 | self.debounce(for: interval, tolerance: tolerance, clock: .continuous) 33 | } 34 | } 35 | 36 | /// An `AsyncSequence` that emits the latest element after a given quiescence period 37 | /// has elapsed. 38 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 39 | public struct AsyncDebounceSequence: Sendable where Base.Element: Sendable { 40 | private let base: Base 41 | private let clock: C 42 | private let interval: C.Instant.Duration 43 | private let tolerance: C.Instant.Duration? 44 | 45 | /// Initializes a new ``AsyncDebounceSequence``. 46 | /// 47 | /// - Parameters: 48 | /// - base: The base sequence. 49 | /// - interval: The interval to debounce. 50 | /// - tolerance: The tolerance of the clock. 51 | /// - clock: The clock. 52 | init(_ base: Base, interval: C.Instant.Duration, tolerance: C.Instant.Duration?, clock: C) { 53 | self.base = base 54 | self.interval = interval 55 | self.tolerance = tolerance 56 | self.clock = clock 57 | } 58 | } 59 | 60 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 61 | extension AsyncDebounceSequence: AsyncSequence { 62 | public typealias Element = Base.Element 63 | 64 | public func makeAsyncIterator() -> Iterator { 65 | let storage = DebounceStorage( 66 | base: self.base, 67 | interval: self.interval, 68 | tolerance: self.tolerance, 69 | clock: self.clock 70 | ) 71 | return Iterator(storage: storage) 72 | } 73 | } 74 | 75 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 76 | extension AsyncDebounceSequence { 77 | public struct Iterator: AsyncIteratorProtocol { 78 | /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. 79 | /// 80 | /// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncIterator`` struct itself. 81 | final class InternalClass: Sendable { 82 | private let storage: DebounceStorage 83 | 84 | fileprivate init(storage: DebounceStorage) { 85 | self.storage = storage 86 | } 87 | 88 | deinit { 89 | self.storage.iteratorDeinitialized() 90 | } 91 | 92 | func next() async rethrows -> Element? { 93 | try await self.storage.next() 94 | } 95 | } 96 | 97 | let internalClass: InternalClass 98 | 99 | fileprivate init(storage: DebounceStorage) { 100 | self.internalClass = InternalClass(storage: storage) 101 | } 102 | 103 | public mutating func next() async rethrows -> Element? { 104 | try await self.internalClass.next() 105 | } 106 | } 107 | } 108 | 109 | @available(*, unavailable) 110 | extension AsyncDebounceSequence.Iterator: Sendable {} 111 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/Dictionary.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | extension Dictionary { 14 | /// Creates a new dictionary from the key-value pairs in the given asynchronous sequence. 15 | /// 16 | /// You use this initializer to create a dictionary when you have an asynchronous sequence 17 | /// of key-value tuples with unique keys. Passing an asynchronous sequence with duplicate 18 | /// keys to this initializer results in a runtime error. If your 19 | /// asynchronous sequence might have duplicate keys, use the 20 | /// `Dictionary(_:uniquingKeysWith:)` initializer instead. 21 | /// 22 | /// - Parameter keysAndValues: An asynchronous sequence of key-value pairs to use for 23 | /// the new dictionary. Every key in `keysAndValues` must be unique. 24 | /// - Precondition: The sequence must not have duplicate keys. 25 | @available(AsyncAlgorithms 1.0, *) 26 | @inlinable 27 | public init(uniqueKeysWithValues keysAndValues: S) async rethrows 28 | where S.Element == (Key, Value) { 29 | self.init(uniqueKeysWithValues: try await Array(keysAndValues)) 30 | } 31 | 32 | /// Creates a new dictionary from the key-value pairs in the given asynchronous sequence, 33 | /// using a combining closure to determine the value for any duplicate keys. 34 | /// 35 | /// You use this initializer to create a dictionary when you have a sequence 36 | /// of key-value tuples that might have duplicate keys. As the dictionary is 37 | /// built, the initializer calls the `combine` closure with the current and 38 | /// new values for any duplicate keys. Pass a closure as `combine` that 39 | /// returns the value to use in the resulting dictionary: The closure can 40 | /// choose between the two values, combine them to produce a new value, or 41 | /// even throw an error. 42 | /// 43 | /// - Parameters: 44 | /// - keysAndValues: An asynchronous sequence of key-value pairs to use for the new 45 | /// dictionary. 46 | /// - combine: A closure that is called with the values for any duplicate 47 | /// keys that are encountered. The closure returns the desired value for 48 | /// the final dictionary, or throws an error if building the dictionary 49 | /// can't proceed. 50 | @available(AsyncAlgorithms 1.0, *) 51 | @inlinable 52 | public init( 53 | _ keysAndValues: S, 54 | uniquingKeysWith combine: (Value, Value) async throws -> Value 55 | ) async rethrows where S.Element == (Key, Value) { 56 | self.init() 57 | for try await (key, value) in keysAndValues { 58 | if let existing = self[key] { 59 | self[key] = try await combine(existing, value) 60 | } else { 61 | self[key] = value 62 | } 63 | } 64 | } 65 | 66 | /// Creates a new dictionary whose keys are the groupings returned by the 67 | /// given closure and whose values are arrays of the elements that returned 68 | /// each key. 69 | /// 70 | /// The arrays in the "values" position of the new dictionary each contain at 71 | /// least one element, with the elements in the same order as the source 72 | /// asynchronous sequence. 73 | /// 74 | /// - Parameters: 75 | /// - values: An asynchronous sequence of values to group into a dictionary. 76 | /// - keyForValue: A closure that returns a key for each element in 77 | /// `values`. 78 | @available(AsyncAlgorithms 1.0, *) 79 | @inlinable 80 | public init(grouping values: S, by keyForValue: (S.Element) async throws -> Key) async rethrows 81 | where Value == [S.Element] { 82 | self.init() 83 | for try await value in values { 84 | let key = try await keyForValue(value) 85 | self[key, default: []].append(value) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import DequeModule 13 | 14 | /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences 15 | @available(AsyncAlgorithms 1.0, *) 16 | public func merge( 17 | _ base1: Base1, 18 | _ base2: Base2 19 | ) -> AsyncMerge2Sequence 20 | where 21 | Base1.Element == Base2.Element, 22 | Base1: Sendable, 23 | Base2: Sendable, 24 | Base1.Element: Sendable 25 | { 26 | return AsyncMerge2Sequence(base1, base2) 27 | } 28 | 29 | /// An `AsyncSequence` that takes two upstream `AsyncSequence`s and combines their elements. 30 | @available(AsyncAlgorithms 1.0, *) 31 | public struct AsyncMerge2Sequence< 32 | Base1: AsyncSequence, 33 | Base2: AsyncSequence 34 | >: Sendable 35 | where 36 | Base1.Element == Base2.Element, 37 | Base1: Sendable, 38 | Base2: Sendable, 39 | Base1.Element: Sendable 40 | { 41 | public typealias Element = Base1.Element 42 | 43 | private let base1: Base1 44 | private let base2: Base2 45 | 46 | /// Initializes a new ``AsyncMerge2Sequence``. 47 | /// 48 | /// - Parameters: 49 | /// - base1: The first upstream ``Swift/AsyncSequence``. 50 | /// - base2: The second upstream ``Swift/AsyncSequence``. 51 | init( 52 | _ base1: Base1, 53 | _ base2: Base2 54 | ) { 55 | self.base1 = base1 56 | self.base2 = base2 57 | } 58 | } 59 | 60 | @available(AsyncAlgorithms 1.0, *) 61 | extension AsyncMerge2Sequence: AsyncSequence { 62 | @available(AsyncAlgorithms 1.0, *) 63 | public func makeAsyncIterator() -> Iterator { 64 | let storage = MergeStorage( 65 | base1: base1, 66 | base2: base2, 67 | base3: nil 68 | ) 69 | return Iterator(storage: storage) 70 | } 71 | } 72 | 73 | @available(AsyncAlgorithms 1.0, *) 74 | extension AsyncMerge2Sequence { 75 | @available(AsyncAlgorithms 1.0, *) 76 | public struct Iterator: AsyncIteratorProtocol { 77 | /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. 78 | /// 79 | /// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncIterator`` struct itself. 80 | final class InternalClass: Sendable { 81 | private let storage: MergeStorage 82 | 83 | fileprivate init(storage: MergeStorage) { 84 | self.storage = storage 85 | } 86 | 87 | deinit { 88 | self.storage.iteratorDeinitialized() 89 | } 90 | 91 | func next() async rethrows -> Element? { 92 | try await storage.next() 93 | } 94 | } 95 | 96 | let internalClass: InternalClass 97 | 98 | fileprivate init(storage: MergeStorage) { 99 | internalClass = InternalClass(storage: storage) 100 | } 101 | 102 | public mutating func next() async rethrows -> Element? { 103 | try await internalClass.next() 104 | } 105 | } 106 | } 107 | 108 | @available(*, unavailable) 109 | extension AsyncMerge2Sequence.Iterator: Sendable {} 110 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import DequeModule 13 | 14 | /// Creates an asynchronous sequence of elements from two underlying asynchronous sequences 15 | @available(AsyncAlgorithms 1.0, *) 16 | public func merge< 17 | Base1: AsyncSequence, 18 | Base2: AsyncSequence, 19 | Base3: AsyncSequence 20 | >(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncMerge3Sequence 21 | where 22 | Base1.Element == Base2.Element, 23 | Base1.Element == Base3.Element, 24 | Base1: Sendable, 25 | Base2: Sendable, 26 | Base3: Sendable, 27 | Base1.Element: Sendable 28 | { 29 | return AsyncMerge3Sequence(base1, base2, base3) 30 | } 31 | 32 | /// An `AsyncSequence` that takes three upstream `AsyncSequence`s and combines their elements. 33 | @available(AsyncAlgorithms 1.0, *) 34 | public struct AsyncMerge3Sequence< 35 | Base1: AsyncSequence, 36 | Base2: AsyncSequence, 37 | Base3: AsyncSequence 38 | >: Sendable 39 | where 40 | Base1.Element == Base2.Element, 41 | Base1.Element == Base3.Element, 42 | Base1: Sendable, 43 | Base2: Sendable, 44 | Base3: Sendable, 45 | Base1.Element: Sendable 46 | { 47 | public typealias Element = Base1.Element 48 | 49 | private let base1: Base1 50 | private let base2: Base2 51 | private let base3: Base3 52 | 53 | /// Initializes a new ``AsyncMerge2Sequence``. 54 | /// 55 | /// - Parameters: 56 | /// - base1: The first upstream ``Swift/AsyncSequence``. 57 | /// - base2: The second upstream ``Swift/AsyncSequence``. 58 | /// - base3: The third upstream ``Swift/AsyncSequence``. 59 | init( 60 | _ base1: Base1, 61 | _ base2: Base2, 62 | _ base3: Base3 63 | ) { 64 | self.base1 = base1 65 | self.base2 = base2 66 | self.base3 = base3 67 | } 68 | } 69 | 70 | @available(AsyncAlgorithms 1.0, *) 71 | extension AsyncMerge3Sequence: AsyncSequence { 72 | @available(AsyncAlgorithms 1.0, *) 73 | public func makeAsyncIterator() -> Iterator { 74 | let storage = MergeStorage( 75 | base1: base1, 76 | base2: base2, 77 | base3: base3 78 | ) 79 | return Iterator(storage: storage) 80 | } 81 | } 82 | 83 | @available(AsyncAlgorithms 1.0, *) 84 | extension AsyncMerge3Sequence { 85 | @available(AsyncAlgorithms 1.0, *) 86 | public struct Iterator: AsyncIteratorProtocol { 87 | /// This class is needed to hook the deinit to observe once all references to the ``AsyncIterator`` are dropped. 88 | /// 89 | /// If we get move-only types we should be able to drop this class and use the `deinit` of the ``AsyncIterator`` struct itself. 90 | final class InternalClass: Sendable { 91 | private let storage: MergeStorage 92 | 93 | fileprivate init(storage: MergeStorage) { 94 | self.storage = storage 95 | } 96 | 97 | deinit { 98 | self.storage.iteratorDeinitialized() 99 | } 100 | 101 | func next() async rethrows -> Element? { 102 | try await storage.next() 103 | } 104 | } 105 | 106 | let internalClass: InternalClass 107 | 108 | fileprivate init(storage: MergeStorage) { 109 | internalClass = InternalClass(storage: storage) 110 | } 111 | 112 | public mutating func next() async rethrows -> Element? { 113 | try await internalClass.next() 114 | } 115 | } 116 | } 117 | 118 | @available(*, unavailable) 119 | extension AsyncMerge3Sequence.Iterator: Sendable {} 120 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/RangeReplaceableCollection.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | extension RangeReplaceableCollection { 14 | /// Creates a new instance of a collection containing the elements of an asynchronous sequence. 15 | /// 16 | /// - Parameter source: The asynchronous sequence of elements for the new collection. 17 | @inlinable 18 | public init(_ source: Source) async rethrows where Source.Element == Element { 19 | self.init() 20 | for try await item in source { 21 | append(item) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/Rethrow.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | // This is a hack around the fact that we don't have generic effects 13 | // alternatively in the use cases we would want `rethrows(unsafe)` 14 | // or something like that to avoid this nifty hack... 15 | 16 | @rethrows 17 | internal protocol _ErrorMechanism { 18 | associatedtype Output 19 | func get() throws -> Output 20 | } 21 | 22 | extension _ErrorMechanism { 23 | // rethrow an error only in the cases where it is known to be reachable 24 | internal func _rethrowError() rethrows -> Never { 25 | _ = try _rethrowGet() 26 | fatalError("materialized error without being in a throwing context") 27 | } 28 | 29 | internal func _rethrowGet() rethrows -> Output { 30 | return try get() 31 | } 32 | } 33 | 34 | extension Result: _ErrorMechanism {} 35 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/SetAlgebra.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | extension SetAlgebra { 14 | /// Creates a new set from an asynchronous sequence of items. 15 | /// 16 | /// Use this initializer to create a new set from an asynchronous sequence 17 | /// 18 | /// - Parameter source: The elements to use as members of the new set. 19 | @inlinable 20 | public init(_ source: Source) async rethrows where Source.Element == Element { 21 | self.init() 22 | for try await item in source { 23 | insert(item) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/UnsafeTransfer.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | /// A wrapper struct to unconditionally to transfer an non-Sendable value. 13 | struct UnsafeTransfer: @unchecked Sendable { 14 | let wrapped: Element 15 | 16 | init(_ wrapped: Element) { 17 | self.wrapped = wrapped 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | /// Creates an asynchronous sequence that concurrently awaits values from two `AsyncSequence` types 13 | /// and emits a tuple of the values. 14 | @available(AsyncAlgorithms 1.0, *) 15 | public func zip( 16 | _ base1: Base1, 17 | _ base2: Base2 18 | ) -> AsyncZip2Sequence { 19 | AsyncZip2Sequence(base1, base2) 20 | } 21 | 22 | /// An asynchronous sequence that concurrently awaits values from two `AsyncSequence` types 23 | /// and emits a tuple of the values. 24 | @available(AsyncAlgorithms 1.0, *) 25 | public struct AsyncZip2Sequence: AsyncSequence, Sendable 26 | where Base1: Sendable, Base1.Element: Sendable, Base2: Sendable, Base2.Element: Sendable { 27 | public typealias Element = (Base1.Element, Base2.Element) 28 | public typealias AsyncIterator = Iterator 29 | 30 | let base1: Base1 31 | let base2: Base2 32 | 33 | init(_ base1: Base1, _ base2: Base2) { 34 | self.base1 = base1 35 | self.base2 = base2 36 | } 37 | 38 | public func makeAsyncIterator() -> AsyncIterator { 39 | Iterator(storage: .init(self.base1, self.base2, nil)) 40 | } 41 | 42 | public struct Iterator: AsyncIteratorProtocol { 43 | final class InternalClass { 44 | private let storage: ZipStorage 45 | 46 | fileprivate init(storage: ZipStorage) { 47 | self.storage = storage 48 | } 49 | 50 | deinit { 51 | self.storage.iteratorDeinitialized() 52 | } 53 | 54 | func next() async rethrows -> Element? { 55 | guard let element = try await self.storage.next() else { 56 | return nil 57 | } 58 | 59 | return (element.0, element.1) 60 | } 61 | } 62 | 63 | let internalClass: InternalClass 64 | 65 | fileprivate init(storage: ZipStorage) { 66 | self.internalClass = InternalClass(storage: storage) 67 | } 68 | 69 | public mutating func next() async rethrows -> Element? { 70 | try await self.internalClass.next() 71 | } 72 | } 73 | } 74 | 75 | @available(*, unavailable) 76 | extension AsyncZip2Sequence.Iterator: Sendable {} 77 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | /// Creates an asynchronous sequence that concurrently awaits values from three `AsyncSequence` types 13 | /// and emits a tuple of the values. 14 | @available(AsyncAlgorithms 1.0, *) 15 | public func zip( 16 | _ base1: Base1, 17 | _ base2: Base2, 18 | _ base3: Base3 19 | ) -> AsyncZip3Sequence { 20 | AsyncZip3Sequence(base1, base2, base3) 21 | } 22 | 23 | /// An asynchronous sequence that concurrently awaits values from three `AsyncSequence` types 24 | /// and emits a tuple of the values. 25 | @available(AsyncAlgorithms 1.0, *) 26 | public struct AsyncZip3Sequence: AsyncSequence, 27 | Sendable 28 | where 29 | Base1: Sendable, 30 | Base1.Element: Sendable, 31 | Base2: Sendable, 32 | Base2.Element: Sendable, 33 | Base3: Sendable, 34 | Base3.Element: Sendable 35 | { 36 | public typealias Element = (Base1.Element, Base2.Element, Base3.Element) 37 | public typealias AsyncIterator = Iterator 38 | 39 | let base1: Base1 40 | let base2: Base2 41 | let base3: Base3 42 | 43 | init(_ base1: Base1, _ base2: Base2, _ base3: Base3) { 44 | self.base1 = base1 45 | self.base2 = base2 46 | self.base3 = base3 47 | } 48 | 49 | public func makeAsyncIterator() -> AsyncIterator { 50 | Iterator( 51 | storage: .init(self.base1, self.base2, self.base3) 52 | ) 53 | } 54 | 55 | public struct Iterator: AsyncIteratorProtocol { 56 | final class InternalClass { 57 | private let storage: ZipStorage 58 | 59 | fileprivate init(storage: ZipStorage) { 60 | self.storage = storage 61 | } 62 | 63 | deinit { 64 | self.storage.iteratorDeinitialized() 65 | } 66 | 67 | func next() async rethrows -> Element? { 68 | guard let element = try await self.storage.next() else { 69 | return nil 70 | } 71 | 72 | // This force unwrap is safe since there must be a third element. 73 | return (element.0, element.1, element.2!) 74 | } 75 | } 76 | 77 | let internalClass: InternalClass 78 | 79 | fileprivate init(storage: ZipStorage) { 80 | self.internalClass = InternalClass(storage: storage) 81 | } 82 | 83 | public mutating func next() async rethrows -> Element? { 84 | try await self.internalClass.next() 85 | } 86 | } 87 | } 88 | 89 | @available(*, unavailable) 90 | extension AsyncZip3Sequence.Iterator: Sendable {} 91 | -------------------------------------------------------------------------------- /Sources/AsyncAlgorithms_XCTest/ValidationTest.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import XCTest 13 | import AsyncAlgorithms 14 | import AsyncSequenceValidation 15 | 16 | extension XCTestCase { 17 | func recordFailure(_ description: String, system: Bool = false, at location: AsyncSequenceValidation.SourceLocation) { 18 | #if canImport(Darwin) 19 | let context = XCTSourceCodeContext( 20 | location: XCTSourceCodeLocation(filePath: location.file.description, lineNumber: Int(location.line)) 21 | ) 22 | let issue = XCTIssue( 23 | type: system ? .system : .assertionFailure, 24 | compactDescription: description, 25 | detailedDescription: nil, 26 | sourceCodeContext: context, 27 | associatedError: nil, 28 | attachments: [] 29 | ) 30 | record(issue) 31 | #else 32 | XCTFail(description, file: location.file, line: location.line) 33 | #endif 34 | } 35 | 36 | @available(AsyncAlgorithms 1.0, *) 37 | func validate( 38 | theme: Theme, 39 | expectedFailures: Set, 40 | @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, 41 | file: StaticString = #file, 42 | line: UInt = #line 43 | ) { 44 | var expectations = expectedFailures 45 | var result: AsyncSequenceValidationDiagram.ExpectationResult? 46 | var failures = [AsyncSequenceValidationDiagram.ExpectationFailure]() 47 | let baseLoc = AsyncSequenceValidation.SourceLocation(file: file, line: line) 48 | var accountedFailures = [AsyncSequenceValidationDiagram.ExpectationFailure]() 49 | do { 50 | (result, failures) = try AsyncSequenceValidationDiagram.test(theme: theme, build) 51 | for failure in failures { 52 | if expectations.remove(failure.description) == nil { 53 | recordFailure(failure.description, at: failure.specification?.location ?? baseLoc) 54 | } else { 55 | accountedFailures.append(failure) 56 | } 57 | } 58 | } catch { 59 | if expectations.remove("\(error)") == nil { 60 | recordFailure("\(error)", system: true, at: (error as? SourceFailure)?.location ?? baseLoc) 61 | } 62 | } 63 | // If no failures were expected and the result reconstitues to something different 64 | // than what was expected, dump that out as a failure for easier diagnostics, this 65 | // likely should be done via attachments but that does not display inline code 66 | // nicely. Ideally we would want to have this display as a runtime warning but those 67 | // do not have source line attribution; for now XCTFail is good enough. 68 | if let result = result, expectedFailures.count == 0 { 69 | let expected = result.reconstituteExpected(theme: theme) 70 | let actual = result.reconstituteActual(theme: theme) 71 | if expected != actual { 72 | XCTFail("Validation failure:\nExpected:\n\(expected)\nActual:\n\(actual)", file: file, line: line) 73 | } 74 | } 75 | // any remaining expectations are failures that were expected but did not happen 76 | for expectation in expectations { 77 | XCTFail("Expected failure: \(expectation) did not occur.", file: file, line: line) 78 | } 79 | } 80 | 81 | @available(AsyncAlgorithms 1.0, *) 82 | func validate( 83 | expectedFailures: Set, 84 | @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, 85 | file: StaticString = #file, 86 | line: UInt = #line 87 | ) { 88 | validate(theme: .ascii, expectedFailures: expectedFailures, build, file: file, line: line) 89 | } 90 | 91 | @available(AsyncAlgorithms 1.0, *) 92 | public func validate( 93 | theme: Theme, 94 | @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, 95 | file: StaticString = #file, 96 | line: UInt = #line 97 | ) { 98 | validate(theme: theme, expectedFailures: [], build, file: file, line: line) 99 | } 100 | 101 | @available(AsyncAlgorithms 1.0, *) 102 | public func validate( 103 | @AsyncSequenceValidationDiagram _ build: (AsyncSequenceValidationDiagram) -> Test, 104 | file: StaticString = #file, 105 | line: UInt = #line 106 | ) { 107 | validate(theme: .ascii, expectedFailures: [], build, file: file, line: line) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/AsyncSequenceValidation/AsyncSequenceValidation.docc/AsyncSequenceValidation.md: -------------------------------------------------------------------------------- 1 | # ``AsyncSequenceValidation`` 2 | 3 | 4 | ## Overview 5 | 6 | Testing is a critical area of focus for any package to make it robust, catch bugs, and explain the expected behaviors in a documented manner. Testing things that are asynchronous can be difficult, testing things that are asynchronous multiple times can be even more difficult. 7 | 8 | Types that implement `AsyncSequence` can often be described in deterministic actions given particular inputs. For the inputs, the events can be described as a discrete set: values, errors being thrown, the terminal state of returning a `nil` value from the iterator, or advancing in time and not doing anything. Likewise, the expected output has a discrete set of events: values, errors being caught, the terminal state of receiving a `nil` value from the iterator, or advancing in time and not doing anything. 9 | 10 | ## Topics 11 | 12 | ### Getting Started 13 | 14 | - 15 | -------------------------------------------------------------------------------- /Sources/AsyncSequenceValidation/Clock.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import AsyncAlgorithms 13 | 14 | @available(AsyncAlgorithms 1.0, *) 15 | extension AsyncSequenceValidationDiagram { 16 | public struct Clock { 17 | let queue: WorkQueue 18 | 19 | init(queue: WorkQueue) { 20 | self.queue = queue 21 | } 22 | } 23 | } 24 | 25 | public protocol TestClock: Sendable { 26 | associatedtype Instant: TestInstant 27 | 28 | var now: Instant { get } 29 | 30 | func sleep(until deadline: Self.Instant, tolerance: Self.Instant.Duration?) async throws 31 | } 32 | 33 | public protocol TestInstant: Equatable { 34 | associatedtype Duration 35 | } 36 | 37 | @available(AsyncAlgorithms 1.0, *) 38 | extension AsyncSequenceValidationDiagram.Clock { 39 | public struct Step: DurationProtocol, Hashable, CustomStringConvertible { 40 | internal var rawValue: Int 41 | 42 | internal init(_ rawValue: Int) { 43 | self.rawValue = rawValue 44 | } 45 | 46 | public static func + (lhs: Step, rhs: Step) -> Step { 47 | return .init(lhs.rawValue + rhs.rawValue) 48 | } 49 | 50 | public static func - (lhs: Step, rhs: Step) -> Step { 51 | .init(lhs.rawValue - rhs.rawValue) 52 | } 53 | 54 | public static func / (lhs: Step, rhs: Int) -> Step { 55 | .init(lhs.rawValue / rhs) 56 | } 57 | 58 | public static func * (lhs: Step, rhs: Int) -> Step { 59 | .init(lhs.rawValue * rhs) 60 | } 61 | 62 | public static func / (lhs: Step, rhs: Step) -> Double { 63 | Double(lhs.rawValue) / Double(rhs.rawValue) 64 | } 65 | 66 | public static func < (lhs: Step, rhs: Step) -> Bool { 67 | lhs.rawValue < rhs.rawValue 68 | } 69 | 70 | public static var zero: Step { .init(0) } 71 | 72 | public static func steps(_ amount: Int) -> Step { 73 | return Step(amount) 74 | } 75 | 76 | public var description: String { 77 | return "step \(rawValue)" 78 | } 79 | } 80 | 81 | public struct Instant: CustomStringConvertible { 82 | public typealias Duration = Step 83 | 84 | let when: Step 85 | 86 | public func advanced(by duration: Step) -> Instant { 87 | Instant(when: when + duration) 88 | } 89 | 90 | public func duration(to other: Instant) -> Step { 91 | other.when - when 92 | } 93 | 94 | public static func < (lhs: Instant, rhs: Instant) -> Bool { 95 | lhs.when < rhs.when 96 | } 97 | 98 | public var description: String { 99 | // the raw value is 1 indexed in execution but we should report it as 0 indexed 100 | return "tick \(when.rawValue - 1)" 101 | } 102 | } 103 | 104 | public var now: Instant { 105 | queue.now 106 | } 107 | 108 | public var minimumResolution: Step { 109 | .steps(1) 110 | } 111 | 112 | public func sleep( 113 | until deadline: Instant, 114 | tolerance: Step? = nil 115 | ) async throws { 116 | let token = queue.prepare() 117 | try await withTaskCancellationHandler { 118 | try await withUnsafeThrowingContinuation { continuation in 119 | queue.enqueue( 120 | AsyncSequenceValidationDiagram.Context.currentJob, 121 | deadline: deadline, 122 | continuation: continuation, 123 | token: token 124 | ) 125 | } 126 | } onCancel: { 127 | queue.cancel(token) 128 | } 129 | } 130 | } 131 | 132 | @available(AsyncAlgorithms 1.0, *) 133 | extension AsyncSequenceValidationDiagram.Clock.Instant: TestInstant {} 134 | 135 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 136 | extension AsyncSequenceValidationDiagram.Clock.Instant: InstantProtocol {} 137 | 138 | @available(AsyncAlgorithms 1.0, *) 139 | extension AsyncSequenceValidationDiagram.Clock: TestClock {} 140 | 141 | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) 142 | extension AsyncSequenceValidationDiagram.Clock: Clock {} 143 | 144 | // placeholders to avoid warnings 145 | @available(AsyncAlgorithms 1.0, *) 146 | extension AsyncSequenceValidationDiagram.Clock.Instant: Hashable {} 147 | @available(AsyncAlgorithms 1.0, *) 148 | extension AsyncSequenceValidationDiagram.Clock.Instant: Comparable {} 149 | -------------------------------------------------------------------------------- /Sources/AsyncSequenceValidation/Input.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | extension AsyncSequenceValidationDiagram { 14 | public struct Specification: Sendable { 15 | public let specification: String 16 | public let location: SourceLocation 17 | 18 | init(specification: String, location: SourceLocation) { 19 | self.specification = specification 20 | self.location = location 21 | } 22 | } 23 | 24 | public struct Input: AsyncSequence, Sendable { 25 | public typealias Element = String 26 | 27 | struct State { 28 | var emissions = [(Clock.Instant, Event)]() 29 | } 30 | 31 | let state = ManagedCriticalState(State()) 32 | let queue: WorkQueue 33 | let index: Int 34 | 35 | public struct Iterator: AsyncIteratorProtocol, Sendable { 36 | let state: ManagedCriticalState 37 | let queue: WorkQueue 38 | let index: Int 39 | var active: (Clock.Instant, [Result])? 40 | var eventIndex = 0 41 | 42 | mutating func apply(when: Clock.Instant, results: [Result]) async throws -> Element? { 43 | let token = queue.prepare() 44 | if eventIndex + 1 >= results.count { 45 | active = nil 46 | } 47 | defer { 48 | if active != nil { 49 | eventIndex += 1 50 | } else { 51 | eventIndex = 0 52 | } 53 | } 54 | return try await withTaskCancellationHandler { 55 | try await withUnsafeThrowingContinuation { continuation in 56 | queue.enqueue( 57 | Context.currentJob, 58 | deadline: when, 59 | continuation: continuation, 60 | results[eventIndex], 61 | index: index, 62 | token: token 63 | ) 64 | } 65 | } onCancel: { [queue] in 66 | queue.cancel(token) 67 | } 68 | } 69 | 70 | public mutating func next() async throws -> Element? { 71 | guard let (when, results) = active else { 72 | let next = state.withCriticalRegion { state -> (Clock.Instant, Event)? in 73 | guard state.emissions.count > 0 else { 74 | return nil 75 | } 76 | return state.emissions.removeFirst() 77 | } 78 | guard let next = next else { 79 | return nil 80 | } 81 | let when = next.0 82 | let results = next.1.results 83 | active = (when, results) 84 | return try await apply(when: when, results: results) 85 | } 86 | return try await apply(when: when, results: results) 87 | } 88 | } 89 | 90 | public func makeAsyncIterator() -> Iterator { 91 | Iterator(state: state, queue: queue, index: index) 92 | } 93 | 94 | func parse(_ dsl: String, theme: Theme, location: SourceLocation) throws { 95 | let emissions = try Event.parse(dsl, theme: theme, location: location) 96 | state.withCriticalRegion { state in 97 | state.emissions = emissions 98 | } 99 | } 100 | 101 | var end: Clock.Instant? { 102 | return state.withCriticalRegion { state in 103 | state.emissions.map { $0.0 }.sorted().last 104 | } 105 | } 106 | } 107 | 108 | public struct InputList: RandomAccessCollection, Sendable { 109 | let state = ManagedCriticalState([Input]()) 110 | let queue: WorkQueue 111 | 112 | public var startIndex: Int { return 0 } 113 | public var endIndex: Int { 114 | state.withCriticalRegion { $0.count } 115 | } 116 | 117 | public subscript(position: Int) -> AsyncSequenceValidationDiagram.Input { 118 | get { 119 | return state.withCriticalRegion { state in 120 | if position >= state.count { 121 | for _ in state.count...position { 122 | state.append(Input(queue: queue, index: position)) 123 | } 124 | } 125 | return state[position] 126 | } 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/AsyncSequenceValidation/Job.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import _CAsyncSequenceValidationSupport 13 | 14 | @available(AsyncAlgorithms 1.0, *) 15 | struct Job: Hashable, @unchecked Sendable { 16 | let job: JobRef 17 | 18 | init(_ job: JobRef) { 19 | self.job = job 20 | } 21 | 22 | func execute() { 23 | _swiftJobRun(unsafeBitCast(job, to: UnownedJob.self), AsyncSequenceValidationDiagram.Context.unownedExecutor) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/AsyncSequenceValidation/Locking.swift: -------------------------------------------------------------------------------- 1 | ../../Sources/AsyncAlgorithms/Locking.swift -------------------------------------------------------------------------------- /Sources/AsyncSequenceValidation/SourceLocation.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | public struct SourceLocation: Sendable, CustomStringConvertible { 13 | public var file: StaticString 14 | public var line: UInt 15 | 16 | public init(file: StaticString, line: UInt) { 17 | self.file = file 18 | self.line = line 19 | } 20 | 21 | public var description: String { 22 | return "\(file):\(line)" 23 | } 24 | } 25 | 26 | public protocol SourceFailure: Error { 27 | var location: SourceLocation { get } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/AsyncSequenceValidation/TaskDriver.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import _CAsyncSequenceValidationSupport 13 | 14 | #if canImport(Darwin) 15 | import Darwin 16 | #elseif canImport(Glibc) 17 | import Glibc 18 | #elseif canImport(Musl) 19 | import Musl 20 | #elseif canImport(Bionic) 21 | import Bionic 22 | #elseif canImport(WinSDK) 23 | #error("TODO: Port TaskDriver threading to windows") 24 | #else 25 | #error("Unsupported platform") 26 | #endif 27 | 28 | #if canImport(Darwin) 29 | @available(AsyncAlgorithms 1.0, *) 30 | func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? { 31 | Unmanaged.fromOpaque(raw).takeRetainedValue().run() 32 | return nil 33 | } 34 | #elseif (canImport(Glibc) && !os(Android)) || canImport(Musl) 35 | @available(AsyncAlgorithms 1.0, *) 36 | func start_thread(_ raw: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? { 37 | Unmanaged.fromOpaque(raw!).takeRetainedValue().run() 38 | return nil 39 | } 40 | #elseif os(Android) 41 | @available(AsyncAlgorithms 1.0, *) 42 | func start_thread(_ raw: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { 43 | Unmanaged.fromOpaque(raw).takeRetainedValue().run() 44 | return UnsafeMutableRawPointer(bitPattern: 0xdeadbee)! 45 | } 46 | #elseif canImport(WinSDK) 47 | #error("TODO: Port TaskDriver threading to windows") 48 | #endif 49 | 50 | @available(AsyncAlgorithms 1.0, *) 51 | final class TaskDriver { 52 | let work: (TaskDriver) -> Void 53 | let queue: WorkQueue 54 | #if canImport(Darwin) 55 | var thread: pthread_t? 56 | #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) 57 | var thread = pthread_t() 58 | #elseif canImport(WinSDK) 59 | #error("TODO: Port TaskDriver threading to windows") 60 | #endif 61 | 62 | init(queue: WorkQueue, _ work: @escaping (TaskDriver) -> Void) { 63 | self.queue = queue 64 | self.work = work 65 | } 66 | 67 | func start() { 68 | #if canImport(Darwin) || canImport(Glibc) || canImport(Musl) || canImport(Bionic) 69 | pthread_create( 70 | &thread, 71 | nil, 72 | start_thread, 73 | Unmanaged.passRetained(self).toOpaque() 74 | ) 75 | #elseif canImport(WinSDK) 76 | #error("TODO: Port TaskDriver threading to windows") 77 | #endif 78 | } 79 | 80 | func run() { 81 | #if canImport(Darwin) 82 | pthread_setname_np("Validation Diagram Clock Driver") 83 | #endif 84 | work(self) 85 | } 86 | 87 | func join() { 88 | #if canImport(Darwin) 89 | pthread_join(thread!, nil) 90 | #elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) 91 | pthread_join(thread, nil) 92 | #elseif canImport(WinSDK) 93 | #error("TODO: Port TaskDriver threading to windows") 94 | #endif 95 | } 96 | 97 | func enqueue(_ job: JobRef) { 98 | let job = Job(job) 99 | queue.enqueue(AsyncSequenceValidationDiagram.Context.currentJob) { 100 | let previous = AsyncSequenceValidationDiagram.Context.currentJob 101 | AsyncSequenceValidationDiagram.Context.currentJob = job 102 | job.execute() 103 | AsyncSequenceValidationDiagram.Context.currentJob = previous 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/AsyncSequenceValidation/Theme.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | @available(AsyncAlgorithms 1.0, *) 13 | public protocol AsyncSequenceValidationTheme { 14 | func token(_ character: Character, inValue: Bool) -> AsyncSequenceValidationDiagram.Token 15 | 16 | func description(for token: AsyncSequenceValidationDiagram.Token) -> String 17 | } 18 | 19 | @available(AsyncAlgorithms 1.0, *) 20 | extension AsyncSequenceValidationTheme where Self == AsyncSequenceValidationDiagram.ASCIITheme { 21 | public static var ascii: AsyncSequenceValidationDiagram.ASCIITheme { 22 | return AsyncSequenceValidationDiagram.ASCIITheme() 23 | } 24 | } 25 | 26 | @available(AsyncAlgorithms 1.0, *) 27 | extension AsyncSequenceValidationDiagram { 28 | public enum Token: Sendable { 29 | case step 30 | case error 31 | case finish 32 | case cancel 33 | case delayNext 34 | case beginValue 35 | case endValue 36 | case beginGroup 37 | case endGroup 38 | case skip 39 | case value(String) 40 | } 41 | 42 | public struct ASCIITheme: AsyncSequenceValidationTheme, Sendable { 43 | public func token(_ character: Character, inValue: Bool) -> AsyncSequenceValidationDiagram.Token { 44 | switch character { 45 | case "-": return .step 46 | case "^": return .error 47 | case "|": return .finish 48 | case ";": return .cancel 49 | case ",": return .delayNext 50 | case "'": return inValue ? .endValue : .beginValue 51 | case "[": return .beginGroup 52 | case "]": return .endGroup 53 | case " ": return .skip 54 | default: return .value(String(character)) 55 | } 56 | } 57 | 58 | public func description(for token: AsyncSequenceValidationDiagram.Token) -> String { 59 | switch token { 60 | case .step: return "-" 61 | case .error: return "^" 62 | case .finish: return "|" 63 | case .cancel: return ";" 64 | case .delayNext: return "," 65 | case .beginValue: return "'" 66 | case .endValue: return "'" 67 | case .beginGroup: return "[" 68 | case .endGroup: return "]" 69 | case .skip: return " " 70 | case .value(let value): return value 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/_CAsyncSequenceValidationSupport/module.modulemap: -------------------------------------------------------------------------------- 1 | module _CAsyncSequenceValidationSupport [system] { 2 | header "_CAsyncSequenceValidationSupport.h" 3 | export * 4 | } 5 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/Performance/TestThroughput.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import XCTest 13 | import AsyncAlgorithms 14 | 15 | #if canImport(Darwin) 16 | final class TestThroughput: XCTestCase { 17 | func test_channel() async { 18 | await measureChannelThroughput(output: 1) 19 | } 20 | func test_throwingChannel() async { 21 | await measureThrowingChannelThroughput(output: 1) 22 | } 23 | func test_buffer_bounded() async { 24 | await measureSequenceThroughput(output: 1) { 25 | $0.buffer(policy: .bounded(5)) 26 | } 27 | } 28 | func test_buffer_unbounded() async { 29 | await measureSequenceThroughput(output: 1) { 30 | $0.buffer(policy: .unbounded) 31 | } 32 | } 33 | func test_buffer_bufferingNewest() async { 34 | await measureSequenceThroughput(output: 1) { 35 | $0.buffer(policy: .bufferingLatest(5)) 36 | } 37 | } 38 | func test_buffer_bufferingOldest() async { 39 | await measureSequenceThroughput(output: 1) { 40 | $0.buffer(policy: .bufferingOldest(5)) 41 | } 42 | } 43 | func test_chain2() async { 44 | await measureSequenceThroughput(firstOutput: 1, secondOutput: 2) { 45 | chain($0, $1) 46 | } 47 | } 48 | func test_chain3() async { 49 | await measureSequenceThroughput(firstOutput: 1, secondOutput: 2, thirdOutput: 3) { 50 | chain($0, $1, $2) 51 | } 52 | } 53 | func test_compacted() async { 54 | await measureSequenceThroughput(output: .some(1)) { 55 | $0.compacted() 56 | } 57 | } 58 | func test_interspersed() async { 59 | await measureSequenceThroughput(output: 1) { 60 | $0.interspersed(with: 0) 61 | } 62 | } 63 | func test_joined() async { 64 | await measureSequenceThroughput(output: [1, 2, 3, 4, 5].async) { 65 | $0.joined(separator: [0, 0, 0, 0, 0].async) 66 | } 67 | } 68 | func test_merge2() async { 69 | await measureSequenceThroughput(firstOutput: 1, secondOutput: 2) { 70 | merge($0, $1) 71 | } 72 | } 73 | func test_merge3() async { 74 | await measureSequenceThroughput(firstOutput: 1, secondOutput: 2, thirdOutput: 3) { 75 | merge($0, $1, $2) 76 | } 77 | } 78 | func test_removeDuplicates() async { 79 | await measureSequenceThroughput(source: (1...).async) { 80 | $0.removeDuplicates() 81 | } 82 | } 83 | func test_zip2() async { 84 | await measureSequenceThroughput(firstOutput: 1, secondOutput: 2) { 85 | zip($0, $1) 86 | } 87 | } 88 | func test_zip3() async { 89 | await measureSequenceThroughput(firstOutput: 1, secondOutput: 2, thirdOutput: 3) { 90 | zip($0, $1, $2) 91 | } 92 | } 93 | func test_debounce() async { 94 | if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { 95 | await measureSequenceThroughput(source: (1...).async) { 96 | $0.debounce(for: .zero, clock: ContinuousClock()) 97 | } 98 | } 99 | } 100 | } 101 | #endif 102 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/Support/Failure.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | struct Failure: Error, Equatable {} 13 | 14 | func throwOn(_ toThrowOn: T, _ value: T) throws -> T { 15 | if value == toThrowOn { 16 | throw Failure() 17 | } 18 | return value 19 | } 20 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/Support/Gate.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import AsyncAlgorithms 13 | 14 | public struct Gate: Sendable { 15 | enum State { 16 | case closed 17 | case open 18 | case pending(UnsafeContinuation) 19 | } 20 | 21 | let state = ManagedCriticalState(State.closed) 22 | 23 | public func `open`() { 24 | state.withCriticalRegion { state -> UnsafeContinuation? in 25 | switch state { 26 | case .closed: 27 | state = .open 28 | return nil 29 | case .open: 30 | return nil 31 | case .pending(let continuation): 32 | state = .closed 33 | return continuation 34 | } 35 | }?.resume() 36 | } 37 | 38 | public func enter() async { 39 | var other: UnsafeContinuation? 40 | await withUnsafeContinuation { (continuation: UnsafeContinuation) in 41 | state.withCriticalRegion { state -> UnsafeContinuation? in 42 | switch state { 43 | case .closed: 44 | state = .pending(continuation) 45 | return nil 46 | case .open: 47 | state = .closed 48 | return continuation 49 | case .pending(let existing): 50 | other = existing 51 | state = .pending(continuation) 52 | return nil 53 | } 54 | }?.resume() 55 | } 56 | other?.resume() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/Support/GatedSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | public struct GatedSequence { 13 | let elements: [Element] 14 | let gates: [Gate] 15 | var index = 0 16 | 17 | public mutating func advance() { 18 | defer { index += 1 } 19 | guard index < gates.count else { 20 | return 21 | } 22 | gates[index].open() 23 | } 24 | 25 | public init(_ elements: [Element]) { 26 | self.elements = elements 27 | self.gates = elements.map { _ in Gate() } 28 | } 29 | } 30 | 31 | extension GatedSequence: AsyncSequence { 32 | public struct Iterator: AsyncIteratorProtocol { 33 | var gatedElements: [(Element, Gate)] 34 | 35 | init(elements: [Element], gates: [Gate]) { 36 | gatedElements = Array(zip(elements, gates)) 37 | } 38 | 39 | public mutating func next() async -> Element? { 40 | guard gatedElements.count > 0 else { 41 | return nil 42 | } 43 | let (element, gate) = gatedElements.removeFirst() 44 | await gate.enter() 45 | return element 46 | } 47 | } 48 | 49 | public func makeAsyncIterator() -> Iterator { 50 | Iterator(elements: elements, gates: gates) 51 | } 52 | } 53 | 54 | extension GatedSequence: Sendable where Element: Sendable {} 55 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/Support/Indefinite.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | struct Indefinite: Sequence, IteratorProtocol, Sendable { 13 | let value: Element 14 | 15 | func next() -> Element? { 16 | return value 17 | } 18 | 19 | func makeIterator() -> Indefinite { 20 | self 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/Support/Locking.swift: -------------------------------------------------------------------------------- 1 | ../../../Sources/AsyncAlgorithms/Locking.swift -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | final class ReportingSequence: Sequence, IteratorProtocol { 13 | enum Event: Equatable, CustomStringConvertible { 14 | case next 15 | case makeIterator 16 | 17 | var description: String { 18 | switch self { 19 | case .next: return "next()" 20 | case .makeIterator: return "makeIterator()" 21 | } 22 | } 23 | } 24 | 25 | var events = [Event]() 26 | var elements: [Element] 27 | 28 | init(_ elements: [Element]) { 29 | self.elements = elements 30 | } 31 | 32 | func next() -> Element? { 33 | events.append(.next) 34 | guard elements.count > 0 else { 35 | return nil 36 | } 37 | return elements.removeFirst() 38 | } 39 | 40 | func makeIterator() -> ReportingSequence { 41 | events.append(.makeIterator) 42 | return self 43 | } 44 | } 45 | 46 | final class ReportingAsyncSequence: AsyncSequence, AsyncIteratorProtocol, @unchecked Sendable { 47 | enum Event: Equatable, CustomStringConvertible { 48 | case next 49 | case makeAsyncIterator 50 | 51 | var description: String { 52 | switch self { 53 | case .next: return "next()" 54 | case .makeAsyncIterator: return "makeAsyncIterator()" 55 | } 56 | } 57 | } 58 | 59 | var events = [Event]() 60 | var elements: [Element] 61 | 62 | init(_ elements: [Element]) { 63 | self.elements = elements 64 | } 65 | 66 | func next() async -> Element? { 67 | events.append(.next) 68 | guard elements.count > 0 else { 69 | return nil 70 | } 71 | return elements.removeFirst() 72 | } 73 | 74 | func makeAsyncIterator() -> ReportingAsyncSequence { 75 | events.append(.makeAsyncIterator) 76 | return self 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/Support/Validator.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import AsyncAlgorithms 13 | 14 | public struct Validator: Sendable { 15 | private enum Ready { 16 | case idle 17 | case ready 18 | case pending(UnsafeContinuation) 19 | } 20 | 21 | private struct State: Sendable { 22 | var collected = [Element]() 23 | var failure: Error? 24 | var ready: Ready = .idle 25 | } 26 | 27 | private struct Envelope: @unchecked Sendable { 28 | var contents: Contents 29 | } 30 | 31 | private let state = ManagedCriticalState(State()) 32 | 33 | private func ready(_ apply: (inout State) -> Void) { 34 | state.withCriticalRegion { state -> UnsafeContinuation? in 35 | apply(&state) 36 | switch state.ready { 37 | case .idle: 38 | state.ready = .ready 39 | return nil 40 | case .pending(let continuation): 41 | state.ready = .idle 42 | return continuation 43 | case .ready: 44 | return nil 45 | } 46 | }?.resume() 47 | } 48 | 49 | internal func step() async { 50 | await withUnsafeContinuation { (continuation: UnsafeContinuation) in 51 | state.withCriticalRegion { state -> UnsafeContinuation? in 52 | switch state.ready { 53 | case .ready: 54 | state.ready = .idle 55 | return continuation 56 | case .idle: 57 | state.ready = .pending(continuation) 58 | return nil 59 | case .pending: 60 | fatalError() 61 | } 62 | }?.resume() 63 | } 64 | } 65 | 66 | let onEvent: (@Sendable (Result) async -> Void)? 67 | 68 | init(onEvent: @Sendable @escaping (Result) async -> Void) { 69 | 70 | self.onEvent = onEvent 71 | } 72 | 73 | public init() { 74 | self.onEvent = nil 75 | } 76 | 77 | public func test( 78 | _ sequence: S, 79 | onFinish: @Sendable @escaping (inout S.AsyncIterator) async -> Void 80 | ) where S.Element == Element { 81 | let envelope = Envelope(contents: sequence) 82 | Task { 83 | var iterator = envelope.contents.makeAsyncIterator() 84 | ready { _ in } 85 | do { 86 | while let item = try await iterator.next() { 87 | await onEvent?(.success(item)) 88 | ready { state in 89 | state.collected.append(item) 90 | } 91 | } 92 | await onEvent?(.success(nil)) 93 | } catch { 94 | await onEvent?(.failure(error)) 95 | ready { state in 96 | state.failure = error 97 | } 98 | } 99 | ready { _ in } 100 | await onFinish(&iterator) 101 | } 102 | } 103 | 104 | public func validate() async -> [Element] { 105 | await step() 106 | return current 107 | } 108 | 109 | public var current: [Element] { 110 | return state.withCriticalRegion { state in 111 | return state.collected 112 | } 113 | } 114 | 115 | public var failure: Error? { 116 | return state.withCriticalRegion { state in 117 | return state.failure 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/Support/ViolatingSequence.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | extension AsyncSequence { 13 | func violatingSpecification(returningPastEndIteration element: Element) -> SpecificationViolatingSequence { 14 | SpecificationViolatingSequence(self, kind: .producing(element)) 15 | } 16 | 17 | func violatingSpecification(throwingPastEndIteration error: Error) -> SpecificationViolatingSequence { 18 | SpecificationViolatingSequence(self, kind: .throwing(error)) 19 | } 20 | } 21 | 22 | struct SpecificationViolatingSequence { 23 | enum Kind { 24 | case producing(Base.Element) 25 | case throwing(Error) 26 | } 27 | 28 | let base: Base 29 | let kind: Kind 30 | 31 | init(_ base: Base, kind: Kind) { 32 | self.base = base 33 | self.kind = kind 34 | } 35 | } 36 | 37 | extension SpecificationViolatingSequence: AsyncSequence { 38 | typealias Element = Base.Element 39 | 40 | struct Iterator: AsyncIteratorProtocol { 41 | var iterator: Base.AsyncIterator 42 | let kind: Kind 43 | var finished = false 44 | var violated = false 45 | 46 | mutating func next() async throws -> Element? { 47 | if finished { 48 | if violated { 49 | return nil 50 | } 51 | violated = true 52 | switch kind { 53 | case .producing(let element): return element 54 | case .throwing(let error): throw error 55 | } 56 | } 57 | do { 58 | if let value = try await iterator.next() { 59 | return value 60 | } 61 | finished = true 62 | return nil 63 | } catch { 64 | finished = true 65 | throw error 66 | } 67 | } 68 | } 69 | 70 | func makeAsyncIterator() -> Iterator { 71 | Iterator(iterator: base.makeAsyncIterator(), kind: kind) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import XCTest 13 | import AsyncAlgorithms 14 | 15 | final class TestAdjacentPairs: XCTestCase { 16 | func test_adjacentPairs_produces_tuples_of_adjacent_values_of_original_element() async { 17 | let source = 1...5 18 | let expected = Array(zip(source, source.dropFirst())) 19 | 20 | let sequence = source.async.adjacentPairs() 21 | var actual: [(Int, Int)] = [] 22 | for await item in sequence { 23 | actual.append(item) 24 | } 25 | 26 | XCTAssertEqual(expected, actual) 27 | } 28 | 29 | func test_adjacentPairs_forwards_termination_from_source_when_iteration_is_finished() async { 30 | let source = 1...5 31 | 32 | var iterator = source.async.adjacentPairs().makeAsyncIterator() 33 | while let _ = await iterator.next() {} 34 | 35 | let pastEnd = await iterator.next() 36 | XCTAssertNil(pastEnd) 37 | } 38 | 39 | func test_adjacentPairs_produces_empty_sequence_when_source_sequence_is_empty() async { 40 | let source = 0..<1 41 | let expected: [(Int, Int)] = [] 42 | 43 | let sequence = source.async.adjacentPairs() 44 | var actual: [(Int, Int)] = [] 45 | for await item in sequence { 46 | actual.append(item) 47 | } 48 | 49 | XCTAssertEqual(expected, actual) 50 | } 51 | 52 | func test_adjacentPairs_throws_when_source_sequence_throws() async throws { 53 | let source = 1...5 54 | let expected = [(1, 2), (2, 3)] 55 | 56 | let sequence = source.async.map { try throwOn(4, $0) }.adjacentPairs() 57 | var iterator = sequence.makeAsyncIterator() 58 | var actual = [(Int, Int)]() 59 | do { 60 | while let value = try await iterator.next() { 61 | actual.append(value) 62 | } 63 | XCTFail(".adjacentPairs should throw when the source sequence throws") 64 | } catch { 65 | XCTAssertEqual(error as? Failure, Failure()) 66 | } 67 | 68 | XCTAssertEqual(actual, expected) 69 | let pastEnd = try await iterator.next() 70 | XCTAssertNil(pastEnd) 71 | } 72 | 73 | func test_adjacentPairs_finishes_when_iteration_task_is_cancelled() async { 74 | let source = Indefinite(value: 0) 75 | let sequence = source.async.adjacentPairs() 76 | let finished = expectation(description: "finished") 77 | let iterated = expectation(description: "iterated") 78 | 79 | let task = Task { 80 | var firstIteration = false 81 | for await _ in sequence { 82 | if !firstIteration { 83 | firstIteration = true 84 | iterated.fulfill() 85 | } 86 | } 87 | finished.fulfill() 88 | } 89 | 90 | // ensure the other task actually starts 91 | 92 | await fulfillment(of: [iterated], timeout: 1.0) 93 | // cancellation should ensure the loop finishes 94 | // without regards to the remaining underlying sequence 95 | task.cancel() 96 | await fulfillment(of: [finished], timeout: 1.0) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/TestCompacted.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import XCTest 13 | import AsyncAlgorithms 14 | 15 | final class TestCompacted: XCTestCase { 16 | func test_compacted_is_equivalent_to_compactMap_when_input_as_nil_elements() async { 17 | let source = [1, 2, nil, 3, 4, nil, 5] 18 | let expected = source.compactMap { $0 } 19 | let sequence = source.async.compacted() 20 | var actual = [Int]() 21 | for await item in sequence { 22 | actual.append(item) 23 | } 24 | XCTAssertEqual(expected, actual) 25 | } 26 | 27 | func test_compacted_produces_nil_next_element_when_iteration_is_finished() async { 28 | let source = [1, 2, nil, 3, 4, nil, 5] 29 | let expected = source.compactMap { $0 } 30 | let sequence = source.async.compacted() 31 | var actual = [Int]() 32 | var iterator = sequence.makeAsyncIterator() 33 | while let item = await iterator.next() { 34 | actual.append(item) 35 | } 36 | XCTAssertEqual(expected, actual) 37 | let pastEnd = await iterator.next() 38 | XCTAssertNil(pastEnd) 39 | } 40 | 41 | func test_compacted_is_equivalent_to_compactMap_when_input_as_no_nil_elements() async { 42 | let source: [Int?] = [1, 2, 3, 4, 5] 43 | let expected = source.compactMap { $0 } 44 | let sequence = source.async.compacted() 45 | var actual = [Int]() 46 | for await item in sequence { 47 | actual.append(item) 48 | } 49 | XCTAssertEqual(expected, actual) 50 | } 51 | 52 | func test_compacted_throws_when_root_sequence_throws() async throws { 53 | let sequence = [1, nil, 3, 4, 5, nil, 7].async.map { try throwOn(4, $0) }.compacted() 54 | var iterator = sequence.makeAsyncIterator() 55 | var collected = [Int]() 56 | do { 57 | while let value = try await iterator.next() { 58 | collected.append(value) 59 | } 60 | XCTFail() 61 | } catch { 62 | XCTAssertEqual(error as? Failure, Failure()) 63 | } 64 | XCTAssertEqual(collected, [1, 3]) 65 | let pastEnd = try await iterator.next() 66 | XCTAssertNil(pastEnd) 67 | } 68 | 69 | func test_compacted_finishes_when_iteration_task_is_cancelled() async { 70 | let value: String? = "test" 71 | let source = Indefinite(value: value) 72 | let sequence = source.async.compacted() 73 | let finished = expectation(description: "finished") 74 | let iterated = expectation(description: "iterated") 75 | let task = Task { 76 | var firstIteration = false 77 | for await _ in sequence { 78 | if !firstIteration { 79 | firstIteration = true 80 | iterated.fulfill() 81 | } 82 | } 83 | finished.fulfill() 84 | } 85 | // ensure the other task actually starts 86 | await fulfillment(of: [iterated], timeout: 1.0) 87 | // cancellation should ensure the loop finishes 88 | // without regards to the remaining underlying sequence 89 | task.cancel() 90 | await fulfillment(of: [finished], timeout: 1.0) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/TestDebounce.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import XCTest 13 | import AsyncAlgorithms 14 | 15 | final class TestDebounce: XCTestCase { 16 | func test_delayingValues() throws { 17 | guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { 18 | throw XCTSkip("Skipped due to Clock/Instant/Duration availability") 19 | } 20 | validate { 21 | "abcd----e---f-g----|" 22 | $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) 23 | "------d----e-----g-|" 24 | } 25 | } 26 | 27 | func test_delayingValues_dangling_last() throws { 28 | guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { 29 | throw XCTSkip("Skipped due to Clock/Instant/Duration availability") 30 | } 31 | validate { 32 | "abcd----e---f-g-|" 33 | $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) 34 | "------d----e----[g|]" 35 | } 36 | } 37 | 38 | func test_finishDoesntDebounce() throws { 39 | guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { 40 | throw XCTSkip("Skipped due to Clock/Instant/Duration availability") 41 | } 42 | validate { 43 | "a|" 44 | $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) 45 | "-[a|]" 46 | } 47 | } 48 | 49 | func test_throwDoesntDebounce() throws { 50 | guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { 51 | throw XCTSkip("Skipped due to Clock/Instant/Duration availability") 52 | } 53 | validate { 54 | "a^" 55 | $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) 56 | "-^" 57 | } 58 | } 59 | 60 | func test_noValues() throws { 61 | guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { 62 | throw XCTSkip("Skipped due to Clock/Instant/Duration availability") 63 | } 64 | validate { 65 | "----|" 66 | $0.inputs[0].debounce(for: .steps(3), clock: $0.clock) 67 | "----|" 68 | } 69 | } 70 | 71 | func test_Rethrows() async throws { 72 | guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { 73 | throw XCTSkip("Skipped due to Clock/Instant/Duration availability") 74 | } 75 | 76 | let debounce = [1].async.debounce(for: .zero, clock: ContinuousClock()) 77 | for await _ in debounce {} 78 | 79 | let throwingDebounce = [1].async.map { try throwOn(2, $0) }.debounce(for: .zero, clock: ContinuousClock()) 80 | for try await _ in throwingDebounce {} 81 | } 82 | 83 | func test_debounce_when_cancelled() async throws { 84 | guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { 85 | throw XCTSkip("Skipped due to Clock/Instant/Duration availability") 86 | } 87 | 88 | let t = Task { 89 | try? await Task.sleep(nanoseconds: 1_000_000_000) 90 | let c1 = Indefinite(value: "test1").async 91 | for await _ in c1.debounce(for: .seconds(1), clock: .continuous) {} 92 | } 93 | t.cancel() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/TestDictionary.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import XCTest 13 | import AsyncAlgorithms 14 | 15 | final class TestDictionary: XCTestCase { 16 | func test_uniqueKeysAndValues() async { 17 | let source = [(1, "a"), (2, "b"), (3, "c")] 18 | let expected = Dictionary(uniqueKeysWithValues: source) 19 | let actual = await Dictionary(uniqueKeysWithValues: source.async) 20 | XCTAssertEqual(expected, actual) 21 | } 22 | 23 | func test_throwing_uniqueKeysAndValues() async { 24 | let source = Array([1, 2, 3, 4, 5, 6]) 25 | let input = source.async.map { (value: Int) async throws -> (Int, Int) in 26 | if value == 4 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } 27 | return (value, value) 28 | } 29 | do { 30 | _ = try await Dictionary(uniqueKeysWithValues: input) 31 | XCTFail() 32 | } catch { 33 | XCTAssertEqual((error as NSError).code, -1) 34 | } 35 | } 36 | 37 | func test_uniquingWith() async { 38 | let source = [("a", 1), ("b", 2), ("a", 3), ("b", 4)] 39 | let expected = Dictionary(source) { first, _ in first } 40 | let actual = await Dictionary(source.async) { first, _ in first } 41 | XCTAssertEqual(expected, actual) 42 | } 43 | 44 | func test_throwing_uniquingWith() async { 45 | let source = Array([1, 2, 3, 4, 5, 6]) 46 | let input = source.async.map { (value: Int) async throws -> (Int, Int) in 47 | if value == 4 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } 48 | return (value, value) 49 | } 50 | do { 51 | _ = try await Dictionary(input) { first, _ in first } 52 | XCTFail() 53 | } catch { 54 | XCTAssertEqual((error as NSError).code, -1) 55 | } 56 | } 57 | 58 | func test_grouping() async { 59 | let source = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"] 60 | let expected = Dictionary(grouping: source, by: { $0.first! }) 61 | let actual = await Dictionary(grouping: source.async, by: { $0.first! }) 62 | XCTAssertEqual(expected, actual) 63 | } 64 | 65 | func test_throwing_grouping() async { 66 | let source = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"] 67 | let input = source.async.map { (value: String) async throws -> String in 68 | if value == "Kweku" { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } 69 | return value 70 | } 71 | do { 72 | _ = try await Dictionary(grouping: input, by: { $0.first! }) 73 | XCTFail() 74 | } catch { 75 | XCTAssertEqual((error as NSError).code, -1) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/TestLazy.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import XCTest 13 | import AsyncAlgorithms 14 | 15 | final class TestLazy: XCTestCase { 16 | func test_lazy_outputs_elements_and_finishes_when_source_is_array() async { 17 | let expected = [1, 2, 3, 4] 18 | let sequence = expected.async 19 | 20 | var collected = [Int]() 21 | for await item in sequence { 22 | collected.append(item) 23 | } 24 | 25 | XCTAssertEqual(expected, collected) 26 | } 27 | 28 | func test_lazy_outputs_elements_and_finishes_when_source_is_set() async { 29 | let expected: Set = [1, 2, 3, 4] 30 | let sequence = expected.async 31 | 32 | var collected = Set() 33 | for await item in sequence { 34 | collected.insert(item) 35 | } 36 | 37 | XCTAssertEqual(expected, collected) 38 | } 39 | 40 | func test_lazy_finishes_without_elements_when_source_is_empty() async { 41 | let expected = [Int]() 42 | let sequence = expected.async 43 | 44 | var collected = [Int]() 45 | for await item in sequence { 46 | collected.append(item) 47 | } 48 | 49 | XCTAssertEqual(expected, collected) 50 | } 51 | 52 | func test_lazy_triggers_expected_iterator_events_when_source_is_iterated() async { 53 | let expected = [1, 2, 3] 54 | let expectedEvents = [ 55 | ReportingSequence.Event.makeIterator, 56 | .next, 57 | .next, 58 | .next, 59 | .next, 60 | ] 61 | let source = ReportingSequence(expected) 62 | let sequence = source.async 63 | 64 | XCTAssertEqual(source.events, []) 65 | 66 | var collected = [Int]() 67 | for await item in sequence { 68 | collected.append(item) 69 | } 70 | 71 | XCTAssertEqual(expected, collected) 72 | XCTAssertEqual(expectedEvents, source.events) 73 | } 74 | 75 | func test_lazy_stops_triggering_iterator_events_when_source_is_pastEnd() async { 76 | let expected = [1, 2, 3] 77 | let expectedEvents = [ 78 | ReportingSequence.Event.makeIterator, 79 | .next, 80 | .next, 81 | .next, 82 | .next, 83 | ] 84 | let source = ReportingSequence(expected) 85 | let sequence = source.async 86 | 87 | XCTAssertEqual(source.events, []) 88 | 89 | var collected = [Int]() 90 | var iterator = sequence.makeAsyncIterator() 91 | while let item = await iterator.next() { 92 | collected.append(item) 93 | } 94 | 95 | XCTAssertEqual(expected, collected) 96 | XCTAssertEqual(expectedEvents, source.events) 97 | 98 | let pastEnd = await iterator.next() 99 | 100 | XCTAssertEqual(pastEnd, nil) 101 | // ensure that iterating past the end does not invoke next again 102 | XCTAssertEqual(expectedEvents, source.events) 103 | } 104 | 105 | func test_lazy_finishes_when_task_is_cancelled() async { 106 | let finished = expectation(description: "finished") 107 | let iterated = expectation(description: "iterated") 108 | 109 | let source = Indefinite(value: "test") 110 | let sequence = source.async 111 | 112 | let task = Task { 113 | var firstIteration = false 114 | for await _ in sequence { 115 | if !firstIteration { 116 | firstIteration = true 117 | iterated.fulfill() 118 | } 119 | } 120 | finished.fulfill() 121 | } 122 | 123 | // ensure the other task actually starts 124 | await fulfillment(of: [iterated], timeout: 1.0) 125 | 126 | // cancellation should ensure the loop finishes 127 | // without regards to the remaining underlying sequence 128 | task.cancel() 129 | 130 | await fulfillment(of: [finished], timeout: 1.0) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/TestManualClock.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import XCTest 13 | import AsyncAlgorithms 14 | 15 | final class TestManualClock: XCTestCase { 16 | func test_sleep() async { 17 | let clock = ManualClock() 18 | let start = clock.now 19 | let afterSleep = expectation(description: "after sleep") 20 | let state = ManagedCriticalState(false) 21 | Task { 22 | try await clock.sleep(until: start.advanced(by: .steps(3))) 23 | state.withCriticalRegion { $0 = true } 24 | afterSleep.fulfill() 25 | } 26 | XCTAssertFalse(state.withCriticalRegion { $0 }) 27 | clock.advance() 28 | XCTAssertFalse(state.withCriticalRegion { $0 }) 29 | clock.advance() 30 | XCTAssertFalse(state.withCriticalRegion { $0 }) 31 | clock.advance() 32 | await fulfillment(of: [afterSleep], timeout: 1.0) 33 | XCTAssertTrue(state.withCriticalRegion { $0 }) 34 | } 35 | 36 | func test_sleep_cancel() async { 37 | let clock = ManualClock() 38 | let start = clock.now 39 | let afterSleep = expectation(description: "after sleep") 40 | let state = ManagedCriticalState(false) 41 | let failure = ManagedCriticalState(nil) 42 | let task = Task { 43 | do { 44 | try await clock.sleep(until: start.advanced(by: .steps(3))) 45 | } catch { 46 | failure.withCriticalRegion { $0 = error } 47 | } 48 | state.withCriticalRegion { $0 = true } 49 | afterSleep.fulfill() 50 | } 51 | XCTAssertFalse(state.withCriticalRegion { $0 }) 52 | clock.advance() 53 | task.cancel() 54 | await fulfillment(of: [afterSleep], timeout: 1.0) 55 | XCTAssertTrue(state.withCriticalRegion { $0 }) 56 | XCTAssertTrue(failure.withCriticalRegion { $0 is CancellationError }) 57 | } 58 | 59 | func test_sleep_cancel_before_advance() async { 60 | let clock = ManualClock() 61 | let start = clock.now 62 | let afterSleep = expectation(description: "after sleep") 63 | let state = ManagedCriticalState(false) 64 | let failure = ManagedCriticalState(nil) 65 | let task = Task { 66 | do { 67 | try await clock.sleep(until: start.advanced(by: .steps(3))) 68 | } catch { 69 | failure.withCriticalRegion { $0 = error } 70 | } 71 | state.withCriticalRegion { $0 = true } 72 | afterSleep.fulfill() 73 | } 74 | XCTAssertFalse(state.withCriticalRegion { $0 }) 75 | task.cancel() 76 | await fulfillment(of: [afterSleep], timeout: 1.0) 77 | XCTAssertTrue(state.withCriticalRegion { $0 }) 78 | XCTAssertTrue(failure.withCriticalRegion { $0 is CancellationError }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/TestRangeReplaceableCollection.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import XCTest 13 | import AsyncAlgorithms 14 | 15 | final class TestRangeReplaceableCollection: XCTestCase { 16 | func test_String() async { 17 | let source = "abc" 18 | let expected = source 19 | let actual = await String(source.async) 20 | XCTAssertEqual(expected, actual) 21 | } 22 | 23 | func test_Data() async { 24 | let source = Data([1, 2, 3]) 25 | let expected = source 26 | let actual = await Data(source.async) 27 | XCTAssertEqual(expected, actual) 28 | } 29 | 30 | func test_ContiguousArray() async { 31 | let source = ContiguousArray([1, 2, 3]) 32 | let expected = source 33 | let actual = await ContiguousArray(source.async) 34 | XCTAssertEqual(expected, actual) 35 | } 36 | 37 | func test_Array() async { 38 | let source = Array([1, 2, 3]) 39 | let expected = source 40 | let actual = await Array(source.async) 41 | XCTAssertEqual(expected, actual) 42 | } 43 | 44 | func test_throwing() async { 45 | let source = Array([1, 2, 3, 4, 5, 6]) 46 | let input = source.async.map { (value: Int) async throws -> Int in 47 | if value == 4 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } 48 | return value 49 | } 50 | do { 51 | _ = try await Array(input) 52 | XCTFail() 53 | } catch { 54 | XCTAssertEqual((error as NSError).code, -1) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/TestRemoveDuplicates.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import XCTest 13 | import AsyncAlgorithms 14 | 15 | final class TestRemoveDuplicates: XCTestCase { 16 | func test_removeDuplicates() async { 17 | let source = [1, 2, 2, 2, 3, 4, 5, 6, 5, 5] 18 | let expected = [1, 2, 3, 4, 5, 6, 5] 19 | let sequence = source.async.removeDuplicates() 20 | var actual = [Int]() 21 | for await item in sequence { 22 | actual.append(item) 23 | } 24 | XCTAssertEqual(actual, expected) 25 | } 26 | 27 | func test_removeDuplicates_with_closure() async { 28 | let source = [1, 2.001, 2.005, 2.011, 3, 4, 5, 6, 5, 5] 29 | let expected = [1, 2.001, 2.011, 3, 4, 5, 6, 5] 30 | let sequence = source.async.removeDuplicates { abs($0 - $1) < 0.01 } 31 | var actual = [Double]() 32 | for await item in sequence { 33 | actual.append(item) 34 | } 35 | XCTAssertEqual(actual, expected) 36 | } 37 | 38 | func test_removeDuplicates_with_throwing_closure() async { 39 | let source = [1, 2, 2, 2, 3, -1, 5, 6, 5, 5] 40 | let expected = [1, 2, 3] 41 | var actual = [Int]() 42 | let sequence = source.async.removeDuplicates { prev, next in 43 | let next = try throwOn(-1, next) 44 | return prev == next 45 | } 46 | 47 | do { 48 | for try await item in sequence { 49 | actual.append(item) 50 | } 51 | XCTFail() 52 | } catch { 53 | XCTAssertEqual(Failure(), error as? Failure) 54 | } 55 | XCTAssertEqual(actual, expected) 56 | } 57 | 58 | func test_removeDuplicates_with_throwing_upstream() async { 59 | let source = [1, 2, 2, 2, 3, -1, 5, 6, 5, 5] 60 | let expected = [1, 2, 3] 61 | var actual = [Int]() 62 | let throwingSequence = source.async.map( 63 | { 64 | if $0 < 0 { 65 | throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) 66 | } 67 | return $0 68 | } as @Sendable (Int) throws -> Int 69 | ) 70 | 71 | do { 72 | for try await item in throwingSequence.removeDuplicates() { 73 | actual.append(item) 74 | } 75 | XCTFail() 76 | } catch { 77 | XCTAssertEqual((error as NSError).code, -1) 78 | } 79 | XCTAssertEqual(actual, expected) 80 | } 81 | 82 | func test_removeDuplicates_cancellation() async { 83 | let source = Indefinite(value: "test") 84 | let sequence = source.async.removeDuplicates() 85 | let finished = expectation(description: "finished") 86 | let iterated = expectation(description: "iterated") 87 | let task = Task { 88 | var firstIteration = false 89 | for await _ in sequence { 90 | if !firstIteration { 91 | firstIteration = true 92 | iterated.fulfill() 93 | } else { 94 | XCTFail("This sequence should only ever emit a single value") 95 | } 96 | } 97 | finished.fulfill() 98 | } 99 | // ensure the other task actually starts 100 | await fulfillment(of: [iterated], timeout: 1.0) 101 | // cancellation should ensure the loop finishes 102 | // without regards to the remaining underlying sequence 103 | task.cancel() 104 | await fulfillment(of: [finished], timeout: 1.0) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/TestSetAlgebra.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import XCTest 13 | import AsyncAlgorithms 14 | 15 | final class TestSetAlgebra: XCTestCase { 16 | func test_Set() async { 17 | let source = [1, 2, 3] 18 | let expected = Set(source) 19 | let actual = await Set(source.async) 20 | XCTAssertEqual(expected, actual) 21 | } 22 | 23 | func test_Set_duplicate() async { 24 | let source = [1, 2, 3, 3] 25 | let expected = Set(source) 26 | let actual = await Set(source.async) 27 | XCTAssertEqual(expected, actual) 28 | } 29 | 30 | func test_IndexSet() async { 31 | let source = [1, 2, 3] 32 | let expected = IndexSet(source) 33 | let actual = await IndexSet(source.async) 34 | XCTAssertEqual(expected, actual) 35 | } 36 | 37 | func test_throwing() async { 38 | let source = Array([1, 2, 3, 4, 5, 6]) 39 | let input = source.async.map { (value: Int) async throws -> Int in 40 | if value == 4 { throw NSError(domain: NSCocoaErrorDomain, code: -1, userInfo: nil) } 41 | return value 42 | } 43 | do { 44 | _ = try await Set(input) 45 | XCTFail() 46 | } catch { 47 | XCTAssertEqual((error as NSError).code, -1) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/AsyncAlgorithmsTests/TestTimer.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift Async Algorithms open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // 10 | //===----------------------------------------------------------------------===// 11 | 12 | import XCTest 13 | import AsyncAlgorithms 14 | import AsyncSequenceValidation 15 | 16 | final class TestTimer: XCTestCase { 17 | func test_tick1() throws { 18 | guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { 19 | throw XCTSkip("Skipped due to Clock/Instant/Duration availability") 20 | } 21 | validate { 22 | AsyncTimerSequence(interval: .steps(1), clock: $0.clock).map { _ in "x" } 23 | "xxxxxxx[;|]" 24 | } 25 | } 26 | 27 | func test_tick2() throws { 28 | guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { 29 | throw XCTSkip("Skipped due to Clock/Instant/Duration availability") 30 | } 31 | validate { 32 | AsyncTimerSequence(interval: .steps(2), clock: $0.clock).map { _ in "x" } 33 | "-x-x-x-[;|]" 34 | } 35 | } 36 | 37 | func test_tick3() throws { 38 | guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { 39 | throw XCTSkip("Skipped due to Clock/Instant/Duration availability") 40 | } 41 | validate { 42 | AsyncTimerSequence(interval: .steps(3), clock: $0.clock).map { _ in "x" } 43 | "--x--x-[;|]" 44 | } 45 | } 46 | 47 | func test_tick2_event_skew3() throws { 48 | guard #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) else { 49 | throw XCTSkip("Skipped due to Clock/Instant/Duration availability") 50 | } 51 | validate { diagram in 52 | AsyncTimerSequence(interval: .steps(2), clock: diagram.clock).map { [diagram] (_) -> String in 53 | try? await diagram.clock.sleep(until: diagram.clock.now.advanced(by: .steps(3))) 54 | return "x" 55 | } 56 | "----x--x-[;x|]" 57 | } 58 | } 59 | } 60 | --------------------------------------------------------------------------------