From bbbda70bd2fd473f63b69bef2473917cc49dffd7 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Wed, 5 Oct 2022 00:13:20 +0200 Subject: [PATCH 1/6] Make `HTTPClientResponse.init` public --- .../AsyncAwait/AnyAsyncSequence.swift | 48 ++++++++ .../AsyncSequenceFromSyncSequence.swift | 46 +++++++ .../AsyncAwait/HTTPClientResponse.swift | 116 +++++++----------- .../AsyncAwait/Transaction+StateMachine.swift | 16 +-- .../AsyncAwait/Transaction.swift | 6 +- .../AsyncAwait/TransactionBody.swift | 65 ++++++++++ Sources/AsyncHTTPClient/Either.swift | 48 ++++++++ .../HTTPClientRequestTests.swift | 25 ---- 8 files changed, 264 insertions(+), 106 deletions(-) create mode 100644 Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift create mode 100644 Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift create mode 100644 Sources/AsyncHTTPClient/AsyncAwait/TransactionBody.swift create mode 100644 Sources/AsyncHTTPClient/Either.swift diff --git a/Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift b/Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift new file mode 100644 index 000000000..459de2b41 --- /dev/null +++ b/Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@usableFromInline +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +struct AnyAsyncSequence: Sendable, AsyncSequence { + @usableFromInline typealias AsyncIteratorNextCallback = () async throws -> Element? + + @usableFromInline struct AsyncIterator: AsyncIteratorProtocol { + @usableFromInline let nextCallback: AsyncIteratorNextCallback + + @inlinable init(nextCallback: @escaping AsyncIteratorNextCallback) { + self.nextCallback = nextCallback + } + + @inlinable mutating func next() async throws -> Element? { + try await nextCallback() + } + } + + @usableFromInline var makeAsyncIteratorCallback: @Sendable () -> AsyncIteratorNextCallback + + @inlinable public init( + _ asyncSequence: SequenceOfBytes + ) where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == Element { + self.makeAsyncIteratorCallback = { + var iterator = asyncSequence.makeAsyncIterator() + return { + try await iterator.next() + } + } + } + + @inlinable func makeAsyncIterator() -> AsyncIterator { + .init(nextCallback: makeAsyncIteratorCallback()) + } +} diff --git a/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift b/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift new file mode 100644 index 000000000..892766923 --- /dev/null +++ b/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@usableFromInline +struct AsyncSequenceFromSyncSequence: AsyncSequence, Sendable { + @usableFromInline typealias Element = Wrapped.Element + @usableFromInline struct AsyncIterator: AsyncIteratorProtocol { + @usableFromInline var iterator: Wrapped.Iterator + @inlinable init(iterator: Wrapped.Iterator) { + self.iterator = iterator + } + @inlinable mutating func next() async throws -> Wrapped.Element? { + self.iterator.next() + } + } + + @usableFromInline var wrapped: Wrapped + + @inlinable init(wrapped: Wrapped) { + self.wrapped = wrapped + } + + @inlinable func makeAsyncIterator() -> AsyncIterator { + .init(iterator: self.wrapped.makeIterator()) + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension Sequence where Self: Sendable { + /// Turns `self` into an `AsyncSequence` by wending each element of `self` asynchronously. + @inlinable func asAsyncSequence() -> AsyncSequenceFromSyncSequence { + .init(wrapped: self) + } +} diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift index e6cc47210..49c4ea9fa 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift @@ -33,98 +33,74 @@ public struct HTTPClientResponse: Sendable { /// The body of this HTTP response. public var body: Body - /// A representation of the response body for an HTTP response. - /// - /// The body is streamed as an `AsyncSequence` of `ByteBuffer`, where each `ByteBuffer` contains - /// an arbitrarily large chunk of data. The boundaries between `ByteBuffer` objects in the sequence - /// are entirely synthetic and have no semantic meaning. - public struct Body: Sendable { - private let bag: Transaction - private let reference: ResponseRef - - fileprivate init(_ transaction: Transaction) { - self.bag = transaction - self.reference = ResponseRef(transaction: transaction) - } - } - init( bag: Transaction, version: HTTPVersion, status: HTTPResponseStatus, headers: HTTPHeaders ) { - self.body = Body(bag) self.version = version self.status = status self.headers = headers + self.body = Body(TransactionBody(bag)) } -} - -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -extension HTTPClientResponse.Body: AsyncSequence { - public typealias Element = AsyncIterator.Element - - public struct AsyncIterator: AsyncIteratorProtocol { - private let stream: IteratorStream - - fileprivate init(stream: IteratorStream) { - self.stream = stream - } - - public mutating func next() async throws -> ByteBuffer? { - try await self.stream.next() - } - } - - public func makeAsyncIterator() -> AsyncIterator { - AsyncIterator(stream: IteratorStream(bag: self.bag)) + + @inlinable public init(){ + self.version = .http1_1 + self.status = .ok + self.headers = [:] + self.body = Body() } } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -extension HTTPClientResponse.Body { - /// The purpose of this object is to inform the transaction about the response body being deinitialized. - /// If the users has not called `makeAsyncIterator` on the body, before it is deinited, the http - /// request needs to be cancelled. - fileprivate final class ResponseRef: Sendable { - private let transaction: Transaction - - init(transaction: Transaction) { - self.transaction = transaction +extension HTTPClientResponse { + /// A representation of the response body for an HTTP response. + /// + /// The body is streamed as an `AsyncSequence` of `ByteBuffer`, where each `ByteBuffer` contains + /// an arbitrarily large chunk of data. The boundaries between `ByteBuffer` objects in the sequence + /// are entirely synthetic and have no semantic meaning. + public struct Body: AsyncSequence, Sendable { + public typealias Element = ByteBuffer + @usableFromInline typealias Storage = Either> + public struct AsyncIterator: AsyncIteratorProtocol { + @usableFromInline var storage: Storage.AsyncIterator + + @inlinable init(storage: Storage.AsyncIterator) { + self.storage = storage + } + + @inlinable public mutating func next() async throws -> ByteBuffer? { + try await storage.next() + } } + + @usableFromInline var storage: Storage - deinit { - self.transaction.responseBodyDeinited() + @inlinable public func makeAsyncIterator() -> AsyncIterator { + .init(storage: storage.makeAsyncIterator()) } } } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientResponse.Body { - internal class IteratorStream { - struct ID: Hashable { - private let objectID: ObjectIdentifier - - init(_ object: IteratorStream) { - self.objectID = ObjectIdentifier(object) - } - } - - private var id: ID { ID(self) } - private let bag: Transaction - - init(bag: Transaction) { - self.bag = bag - } - - deinit { - self.bag.responseBodyIteratorDeinited(streamID: self.id) - } - - func next() async throws -> ByteBuffer? { - try await self.bag.nextResponsePart(streamID: self.id) - } + @inlinable init(_ body: TransactionBody) { + self.storage = .a(body) + } + + @inlinable public init( + _ sequenceOfBytes: SequenceOfBytes + ) where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == ByteBuffer { + self.storage = .b(AnyAsyncSequence(sequenceOfBytes)) + } + + public init() { + self.init(EmptyCollection().asAsyncSequence()) + } + + public init(_ byteBuffer: ByteBuffer) { + self.init(CollectionOfOne(byteBuffer).asAsyncSequence()) } } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift b/Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift index 38709c9a7..3ec6b4eb7 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift @@ -30,7 +30,7 @@ extension Transaction { case queued(CheckedContinuation, HTTPRequestScheduler) case deadlineExceededWhileQueued(CheckedContinuation) case executing(ExecutionContext, RequestStreamState, ResponseStreamState) - case finished(error: Error?, HTTPClientResponse.Body.IteratorStream.ID?) + case finished(error: Error?, TransactionBody.AsyncIterator.ID?) } fileprivate enum RequestStreamState { @@ -52,9 +52,9 @@ extension Transaction { // We are waiting for the user to create a response body iterator and to call next on // it for the first time. case waitingForResponseIterator(CircularBuffer, next: Next) - case buffering(HTTPClientResponse.Body.IteratorStream.ID, CircularBuffer, next: Next) - case waitingForRemote(HTTPClientResponse.Body.IteratorStream.ID, CheckedContinuation) - case finished(HTTPClientResponse.Body.IteratorStream.ID, CheckedContinuation) + case buffering(TransactionBody.AsyncIterator.ID, CircularBuffer, next: Next) + case waitingForRemote(TransactionBody.AsyncIterator.ID, CheckedContinuation) + case finished(TransactionBody.AsyncIterator.ID, CheckedContinuation) } private var state: State @@ -510,7 +510,7 @@ extension Transaction { } } - mutating func responseBodyIteratorDeinited(streamID: HTTPClientResponse.Body.IteratorStream.ID) -> FailAction { + mutating func responseBodyIteratorDeinited(streamID: TransactionBody.AsyncIterator.ID) -> FailAction { switch self.state { case .initialized, .queued, .deadlineExceededWhileQueued, .executing(_, _, .waitingForResponseHead): preconditionFailure("Got notice about a deinited response body iterator, before we even received a response. Invalid state: \(self.state)") @@ -536,7 +536,7 @@ extension Transaction { } mutating func consumeNextResponsePart( - streamID: HTTPClientResponse.Body.IteratorStream.ID, + streamID: TransactionBody.AsyncIterator.ID, continuation: CheckedContinuation ) -> ConsumeAction { switch self.state { @@ -639,8 +639,8 @@ extension Transaction { } private func verifyStreamIDIsEqual( - registered: HTTPClientResponse.Body.IteratorStream.ID, - this: HTTPClientResponse.Body.IteratorStream.ID, + registered: TransactionBody.AsyncIterator.ID, + this: TransactionBody.AsyncIterator.ID, file: StaticString = #file, line: UInt = #line ) { diff --git a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift index 1974778e9..f5c90557a 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift @@ -20,7 +20,7 @@ import NIOHTTP1 import NIOSSL @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -final class Transaction: @unchecked Sendable { +@usableFromInline final class Transaction: @unchecked Sendable { let logger: Logger let request: HTTPClientRequest.Prepared @@ -334,7 +334,7 @@ extension Transaction { } } - func nextResponsePart(streamID: HTTPClientResponse.Body.IteratorStream.ID) async throws -> ByteBuffer? { + func nextResponsePart(streamID: TransactionBody.AsyncIterator.ID) async throws -> ByteBuffer? { try await withCheckedThrowingContinuation { continuation in let action = self.stateLock.withLock { self.state.consumeNextResponsePart(streamID: streamID, continuation: continuation) @@ -355,7 +355,7 @@ extension Transaction { } } - func responseBodyIteratorDeinited(streamID: HTTPClientResponse.Body.IteratorStream.ID) { + func responseBodyIteratorDeinited(streamID: TransactionBody.AsyncIterator.ID) { let action = self.stateLock.withLock { self.state.responseBodyIteratorDeinited(streamID: streamID) } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/TransactionBody.swift b/Sources/AsyncHTTPClient/AsyncAwait/TransactionBody.swift new file mode 100644 index 000000000..c6e3d34c8 --- /dev/null +++ b/Sources/AsyncHTTPClient/AsyncAwait/TransactionBody.swift @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2021-2022 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore + +/// This is a class because we need to inform the transaction about the response body being deinitialized. +/// If the users has not called `makeAsyncIterator` on the body, before it is deinited, the http +/// request needs to be cancelled. +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@usableFromInline final class TransactionBody: Sendable { + @usableFromInline let transaction: Transaction + + init(_ transaction: Transaction) { + self.transaction = transaction + } + + deinit { + self.transaction.responseBodyDeinited() + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension TransactionBody: AsyncSequence { + @usableFromInline typealias Element = AsyncIterator.Element + + @usableFromInline final class AsyncIterator: AsyncIteratorProtocol { + @usableFromInline struct ID: Hashable { + private let objectID: ObjectIdentifier + + init(_ object: AsyncIterator) { + self.objectID = ObjectIdentifier(object) + } + } + + @usableFromInline var id: ID { ID(self) } + @usableFromInline let transaction: Transaction + + @inlinable init(transaction: Transaction) { + self.transaction = transaction + } + + deinit { + self.transaction.responseBodyIteratorDeinited(streamID: self.id) + } + // TODO: this should be @inlinable + @usableFromInline func next() async throws -> ByteBuffer? { + try await self.transaction.nextResponsePart(streamID: self.id) + } + } + + @inlinable func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(transaction: self.transaction) + } +} diff --git a/Sources/AsyncHTTPClient/Either.swift b/Sources/AsyncHTTPClient/Either.swift new file mode 100644 index 000000000..22a4d0653 --- /dev/null +++ b/Sources/AsyncHTTPClient/Either.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@usableFromInline enum Either { + case a(A) + case b(B) +} + +extension Either: Sendable where A: Sendable, B: Sendable {} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension Either: AsyncSequence where A: AsyncSequence, B: AsyncSequence, A.Element == B.Element { + @usableFromInline typealias Element = A.Element + + @inlinable func makeAsyncIterator() -> Either { + switch self { + case .a(let a): + return .a(a.makeAsyncIterator()) + case .b(let b): + return .b(b.makeAsyncIterator()) + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension Either: AsyncIteratorProtocol where A: AsyncIteratorProtocol, B: AsyncIteratorProtocol, A.Element == B.Element { + @inlinable mutating func next() async throws -> A.Element? { + switch self { + case .a(var a): + defer { self = .a(a) } + return try await a.next() + case .b(var b): + defer { self = .b(b) } + return try await b.next() + } + } +} diff --git a/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift index 42ce4d537..43d03a927 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift @@ -551,29 +551,4 @@ extension Collection { } } -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -struct AsyncSequenceFromSyncSequence: AsyncSequence, Sendable { - typealias Element = Wrapped.Element - struct AsyncIterator: AsyncIteratorProtocol { - fileprivate var iterator: Wrapped.Iterator - mutating func next() async throws -> Wrapped.Element? { - self.iterator.next() - } - } - - fileprivate let wrapped: Wrapped - - func makeAsyncIterator() -> AsyncIterator { - .init(iterator: self.wrapped.makeIterator()) - } -} - -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -extension Sequence where Self: Sendable { - /// Turns `self` into an `AsyncSequence` by wending each element of `self` asynchronously. - func asAsyncSequence() -> AsyncSequenceFromSyncSequence { - .init(wrapped: self) - } -} - #endif From 13e03cd8fd1ccb0c9aeb8331306d9d0c2497acf6 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Wed, 5 Oct 2022 13:31:46 +0200 Subject: [PATCH 2/6] Align `AsyncSequenceFromSyncSequence` with `swift-async-algorithms` --- .../AsyncSequenceFromSyncSequence.swift | 31 ++++++++++++------- .../AsyncAwait/HTTPClientResponse.swift | 4 +-- .../AsyncAwaitEndToEndTests.swift | 6 ++-- .../HTTPClientRequestTests.swift | 4 +-- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift b/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift index 892766923..0ade65547 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift @@ -14,33 +14,40 @@ @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @usableFromInline -struct AsyncSequenceFromSyncSequence: AsyncSequence, Sendable { - @usableFromInline typealias Element = Wrapped.Element +struct AsyncLazySequence: AsyncSequence { + @usableFromInline typealias Element = Base.Element @usableFromInline struct AsyncIterator: AsyncIteratorProtocol { - @usableFromInline var iterator: Wrapped.Iterator - @inlinable init(iterator: Wrapped.Iterator) { + @usableFromInline var iterator: Base.Iterator + @inlinable init(iterator: Base.Iterator) { self.iterator = iterator } - @inlinable mutating func next() async throws -> Wrapped.Element? { + @inlinable mutating func next() async throws -> Base.Element? { self.iterator.next() } } - @usableFromInline var wrapped: Wrapped + @usableFromInline var base: Base - @inlinable init(wrapped: Wrapped) { - self.wrapped = wrapped + @inlinable init(base: Base) { + self.base = base } @inlinable func makeAsyncIterator() -> AsyncIterator { - .init(iterator: self.wrapped.makeIterator()) + .init(iterator: self.base.makeIterator()) } } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -extension Sequence where Self: Sendable { +extension AsyncLazySequence: Sendable where Base: Sendable {} +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension AsyncLazySequence.AsyncIterator: Sendable where Base.Iterator: Sendable {} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension Sequence { /// Turns `self` into an `AsyncSequence` by wending each element of `self` asynchronously. - @inlinable func asAsyncSequence() -> AsyncSequenceFromSyncSequence { - .init(wrapped: self) + @inlinable var async: AsyncLazySequence { + .init(base: self) } } + + diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift index 49c4ea9fa..90ba1937b 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift @@ -96,11 +96,11 @@ extension HTTPClientResponse.Body { } public init() { - self.init(EmptyCollection().asAsyncSequence()) + self.init(EmptyCollection().async) } public init(_ byteBuffer: ByteBuffer) { - self.init(CollectionOfOne(byteBuffer).asAsyncSequence()) + self.init(CollectionOfOne(byteBuffer).async) } } diff --git a/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift b/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift index 92b01dd02..e87d3d392 100644 --- a/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift +++ b/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift @@ -216,7 +216,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase { ByteBuffer(string: "1"), ByteBuffer(string: "2"), ByteBuffer(string: "34"), - ].asAsyncSequence(), length: .unknown) + ].async, length: .unknown) guard let response = await XCTAssertNoThrowWithResult( try await client.execute(request, deadline: .now() + .seconds(10), logger: logger) @@ -241,7 +241,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase { let logger = Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:)) var request = HTTPClientRequest(url: "https://localhost:\(bin.port)/") request.method = .POST - request.body = .stream("1234".utf8.asAsyncSequence(), length: .unknown) + request.body = .stream("1234".utf8.async, length: .unknown) guard let response = await XCTAssertNoThrowWithResult( try await client.execute(request, deadline: .now() + .seconds(10), logger: logger) @@ -614,7 +614,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase { ByteBuffer(string: "1"), ByteBuffer(string: "2"), ByteBuffer(string: "34"), - ].asAsyncSequence(), length: .unknown) + ].async, length: .unknown) guard let response1 = await XCTAssertNoThrowWithResult( try await client.execute(request, deadline: .now() + .seconds(10), logger: logger) diff --git a/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift index 43d03a927..271144cc4 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift @@ -408,7 +408,7 @@ class HTTPClientRequestTests: XCTestCase { let asyncSequence = ByteBuffer(string: "post body") .readableBytesView .chunked(maxChunkSize: 2) - .asAsyncSequence() + .async .map { ByteBuffer($0) } request.body = .stream(asyncSequence, length: .unknown) @@ -449,7 +449,7 @@ class HTTPClientRequestTests: XCTestCase { let asyncSequence = ByteBuffer(string: "post body") .readableBytesView .chunked(maxChunkSize: 2) - .asAsyncSequence() + .async .map { ByteBuffer($0) } request.body = .stream(asyncSequence, length: .known(9)) From 7dfed600f08c917930af6d2d9b8941f059da600e Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Mon, 10 Oct 2022 11:19:41 +0100 Subject: [PATCH 3/6] Address review comments --- .../AsyncAwait/AnyAsyncSequence.swift | 2 +- .../AsyncSequenceFromSyncSequence.swift | 4 +- .../AsyncAwait/HTTPClientResponse.swift | 82 +++++++++++++++---- .../SingleIteratorPrecondition.swift | 43 ++++++++++ Sources/AsyncHTTPClient/Either.swift | 48 ----------- 5 files changed, 113 insertions(+), 66 deletions(-) create mode 100644 Sources/AsyncHTTPClient/AsyncAwait/SingleIteratorPrecondition.swift delete mode 100644 Sources/AsyncHTTPClient/Either.swift diff --git a/Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift b/Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift index 459de2b41..48c8acdb4 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift @@ -31,7 +31,7 @@ struct AnyAsyncSequence: Sendable, AsyncSequence { @usableFromInline var makeAsyncIteratorCallback: @Sendable () -> AsyncIteratorNextCallback - @inlinable public init( + @inlinable init( _ asyncSequence: SequenceOfBytes ) where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == Element { self.makeAsyncIteratorCallback = { diff --git a/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift b/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift index 0ade65547..2aa62b669 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift @@ -2,7 +2,7 @@ // // This source file is part of the AsyncHTTPClient open source project // -// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors +// Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -44,7 +44,7 @@ extension AsyncLazySequence.AsyncIterator: Sendable where Base.Iterator: Sendabl @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension Sequence { - /// Turns `self` into an `AsyncSequence` by wending each element of `self` asynchronously. + /// Turns `self` into an `AsyncSequence` by vending each element of `self` asynchronously. @inlinable var async: AsyncLazySequence { .init(base: self) } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift index 90ba1937b..a48f8b238 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift @@ -45,11 +45,16 @@ public struct HTTPClientResponse: Sendable { self.body = Body(TransactionBody(bag)) } - @inlinable public init(){ - self.version = .http1_1 - self.status = .ok - self.headers = [:] - self.body = Body() + @inlinable public init( + version: HTTPVersion = .http1_1, + status: HTTPResponseStatus = .ok, + headers: HTTPHeaders = [:], + body: Body = Body() + ) { + self.version = version + self.status = status + self.headers = headers + self.body = body } } @@ -62,7 +67,6 @@ extension HTTPClientResponse { /// are entirely synthetic and have no semantic meaning. public struct Body: AsyncSequence, Sendable { public typealias Element = ByteBuffer - @usableFromInline typealias Storage = Either> public struct AsyncIterator: AsyncIteratorProtocol { @usableFromInline var storage: Storage.AsyncIterator @@ -85,22 +89,70 @@ extension HTTPClientResponse { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientResponse.Body { - @inlinable init(_ body: TransactionBody) { - self.storage = .a(body) + @usableFromInline enum Storage: Sendable { + case transaction(SingleIteratorPrecondition) + case anyAsyncSequence(AnyAsyncSequence) } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension HTTPClientResponse.Body.Storage: AsyncSequence { + @usableFromInline typealias Element = ByteBuffer - @inlinable public init( - _ sequenceOfBytes: SequenceOfBytes - ) where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == ByteBuffer { - self.storage = .b(AnyAsyncSequence(sequenceOfBytes)) + @inlinable func makeAsyncIterator() -> AsyncIterator { + switch self { + case .transaction(let transaction): + return .transaction(transaction.makeAsyncIterator()) + case .anyAsyncSequence(let anyAsyncSequence): + return .anyAsyncSequence(anyAsyncSequence.makeAsyncIterator()) + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension HTTPClientResponse.Body.Storage { + @usableFromInline enum AsyncIterator { + case transaction(SingleIteratorPrecondition.AsyncIterator) + case anyAsyncSequence(AnyAsyncSequence.AsyncIterator) + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension HTTPClientResponse.Body.Storage.AsyncIterator: AsyncIteratorProtocol { + @inlinable mutating func next() async throws -> ByteBuffer? { + switch self { + case .transaction(let iterator): + return try await iterator.next() + case .anyAsyncSequence(var iterator): + defer { self = .anyAsyncSequence(iterator) } + return try await iterator.next() + } + } +} + + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension HTTPClientResponse.Body { + init(_ body: TransactionBody) { + self.init(.transaction(body.singleIteratorPrecondition)) + } + + @usableFromInline init(_ storage: Storage) { + self.storage = storage } public init() { - self.init(EmptyCollection().async) + self = .stream(EmptyCollection().async) + } + + @inlinable public static func stream( + _ sequenceOfBytes: SequenceOfBytes + ) -> Self where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == ByteBuffer { + self.init(.anyAsyncSequence(AnyAsyncSequence(sequenceOfBytes.singleIteratorPrecondition))) } - public init(_ byteBuffer: ByteBuffer) { - self.init(CollectionOfOne(byteBuffer).async) + public static func bytes(_ byteBuffer: ByteBuffer) -> Self { + .stream(CollectionOfOne(byteBuffer).async) } } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/SingleIteratorPrecondition.swift b/Sources/AsyncHTTPClient/AsyncAwait/SingleIteratorPrecondition.swift new file mode 100644 index 000000000..eb86edf68 --- /dev/null +++ b/Sources/AsyncHTTPClient/AsyncAwait/SingleIteratorPrecondition.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Atomics + +/// Makes sure that a consumer of this `AsyncSequence` +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@usableFromInline struct SingleIteratorPrecondition: AsyncSequence { + @usableFromInline let base: Base + @usableFromInline let didCreateIterator: ManagedAtomic = .init(false) + @usableFromInline typealias Element = Base.Element + @inlinable init(base: Base) { + self.base = base + } + @inlinable func makeAsyncIterator() -> Base.AsyncIterator { + precondition( + self.didCreateIterator.exchange(true, ordering: .relaxed) == false + ) + return base.makeAsyncIterator() + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension SingleIteratorPrecondition: @unchecked Sendable where Base: Sendable {} + + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension AsyncSequence { + @inlinable var singleIteratorPrecondition: SingleIteratorPrecondition { + .init(base: self) + } +} diff --git a/Sources/AsyncHTTPClient/Either.swift b/Sources/AsyncHTTPClient/Either.swift deleted file mode 100644 index 22a4d0653..000000000 --- a/Sources/AsyncHTTPClient/Either.swift +++ /dev/null @@ -1,48 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the AsyncHTTPClient open source project -// -// Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -@usableFromInline enum Either { - case a(A) - case b(B) -} - -extension Either: Sendable where A: Sendable, B: Sendable {} - -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -extension Either: AsyncSequence where A: AsyncSequence, B: AsyncSequence, A.Element == B.Element { - @usableFromInline typealias Element = A.Element - - @inlinable func makeAsyncIterator() -> Either { - switch self { - case .a(let a): - return .a(a.makeAsyncIterator()) - case .b(let b): - return .b(b.makeAsyncIterator()) - } - } -} - -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -extension Either: AsyncIteratorProtocol where A: AsyncIteratorProtocol, B: AsyncIteratorProtocol, A.Element == B.Element { - @inlinable mutating func next() async throws -> A.Element? { - switch self { - case .a(var a): - defer { self = .a(a) } - return try await a.next() - case .b(var b): - defer { self = .b(b) } - return try await b.next() - } - } -} From c2ffb93773c62f5f759265585b4c42b6819c00e2 Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Mon, 10 Oct 2022 14:59:43 +0100 Subject: [PATCH 4/6] SwiftFormat --- .../AsyncAwait/AnyAsyncSequence.swift | 16 ++++++------- .../AsyncSequenceFromSyncSequence.swift | 7 +++--- .../AsyncAwait/HTTPClientResponse.swift | 23 +++++++++---------- .../SingleIteratorPrecondition.swift | 4 ++-- .../AsyncAwait/TransactionBody.swift | 3 ++- 5 files changed, 26 insertions(+), 27 deletions(-) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift b/Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift index 48c8acdb4..8f6b32bd2 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/AnyAsyncSequence.swift @@ -16,21 +16,21 @@ @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) struct AnyAsyncSequence: Sendable, AsyncSequence { @usableFromInline typealias AsyncIteratorNextCallback = () async throws -> Element? - + @usableFromInline struct AsyncIterator: AsyncIteratorProtocol { @usableFromInline let nextCallback: AsyncIteratorNextCallback - + @inlinable init(nextCallback: @escaping AsyncIteratorNextCallback) { self.nextCallback = nextCallback } - + @inlinable mutating func next() async throws -> Element? { - try await nextCallback() + try await self.nextCallback() } } - + @usableFromInline var makeAsyncIteratorCallback: @Sendable () -> AsyncIteratorNextCallback - + @inlinable init( _ asyncSequence: SequenceOfBytes ) where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == Element { @@ -41,8 +41,8 @@ struct AnyAsyncSequence: Sendable, AsyncSequence { } } } - + @inlinable func makeAsyncIterator() -> AsyncIterator { - .init(nextCallback: makeAsyncIteratorCallback()) + .init(nextCallback: self.makeAsyncIteratorCallback()) } } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift b/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift index 2aa62b669..fe37dd5e7 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift @@ -21,17 +21,18 @@ struct AsyncLazySequence: AsyncSequence { @inlinable init(iterator: Base.Iterator) { self.iterator = iterator } + @inlinable mutating func next() async throws -> Base.Element? { self.iterator.next() } } @usableFromInline var base: Base - + @inlinable init(base: Base) { self.base = base } - + @inlinable func makeAsyncIterator() -> AsyncIterator { .init(iterator: self.base.makeIterator()) } @@ -49,5 +50,3 @@ extension Sequence { .init(base: self) } } - - diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift index a48f8b238..7b291a93a 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift @@ -44,7 +44,7 @@ public struct HTTPClientResponse: Sendable { self.headers = headers self.body = Body(TransactionBody(bag)) } - + @inlinable public init( version: HTTPVersion = .http1_1, status: HTTPResponseStatus = .ok, @@ -69,20 +69,20 @@ extension HTTPClientResponse { public typealias Element = ByteBuffer public struct AsyncIterator: AsyncIteratorProtocol { @usableFromInline var storage: Storage.AsyncIterator - + @inlinable init(storage: Storage.AsyncIterator) { self.storage = storage } - + @inlinable public mutating func next() async throws -> ByteBuffer? { - try await storage.next() + try await self.storage.next() } } - + @usableFromInline var storage: Storage @inlinable public func makeAsyncIterator() -> AsyncIterator { - .init(storage: storage.makeAsyncIterator()) + .init(storage: self.storage.makeAsyncIterator()) } } } @@ -98,7 +98,7 @@ extension HTTPClientResponse.Body { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientResponse.Body.Storage: AsyncSequence { @usableFromInline typealias Element = ByteBuffer - + @inlinable func makeAsyncIterator() -> AsyncIterator { switch self { case .transaction(let transaction): @@ -130,27 +130,26 @@ extension HTTPClientResponse.Body.Storage.AsyncIterator: AsyncIteratorProtocol { } } - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientResponse.Body { init(_ body: TransactionBody) { self.init(.transaction(body.singleIteratorPrecondition)) } - + @usableFromInline init(_ storage: Storage) { self.storage = storage } - + public init() { self = .stream(EmptyCollection().async) } - + @inlinable public static func stream( _ sequenceOfBytes: SequenceOfBytes ) -> Self where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == ByteBuffer { self.init(.anyAsyncSequence(AnyAsyncSequence(sequenceOfBytes.singleIteratorPrecondition))) } - + public static func bytes(_ byteBuffer: ByteBuffer) -> Self { .stream(CollectionOfOne(byteBuffer).async) } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/SingleIteratorPrecondition.swift b/Sources/AsyncHTTPClient/AsyncAwait/SingleIteratorPrecondition.swift index eb86edf68..963fb1f66 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/SingleIteratorPrecondition.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/SingleIteratorPrecondition.swift @@ -23,18 +23,18 @@ import Atomics @inlinable init(base: Base) { self.base = base } + @inlinable func makeAsyncIterator() -> Base.AsyncIterator { precondition( self.didCreateIterator.exchange(true, ordering: .relaxed) == false ) - return base.makeAsyncIterator() + return self.base.makeAsyncIterator() } } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension SingleIteratorPrecondition: @unchecked Sendable where Base: Sendable {} - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension AsyncSequence { @inlinable var singleIteratorPrecondition: SingleIteratorPrecondition { diff --git a/Sources/AsyncHTTPClient/AsyncAwait/TransactionBody.swift b/Sources/AsyncHTTPClient/AsyncAwait/TransactionBody.swift index c6e3d34c8..497a3cc72 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/TransactionBody.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/TransactionBody.swift @@ -24,7 +24,7 @@ import NIOCore init(_ transaction: Transaction) { self.transaction = transaction } - + deinit { self.transaction.responseBodyDeinited() } @@ -53,6 +53,7 @@ extension TransactionBody: AsyncSequence { deinit { self.transaction.responseBodyIteratorDeinited(streamID: self.id) } + // TODO: this should be @inlinable @usableFromInline func next() async throws -> ByteBuffer? { try await self.transaction.nextResponsePart(streamID: self.id) From 09a5977f8ebf0ee62b7d0c5f3df194adc023985b Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Mon, 10 Oct 2022 16:35:35 +0100 Subject: [PATCH 5/6] Rename file to match type name --- ...syncSequenceFromSyncSequence.swift => AsyncLazySequence.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/AsyncHTTPClient/AsyncAwait/{AsyncSequenceFromSyncSequence.swift => AsyncLazySequence.swift} (100%) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift b/Sources/AsyncHTTPClient/AsyncAwait/AsyncLazySequence.swift similarity index 100% rename from Sources/AsyncHTTPClient/AsyncAwait/AsyncSequenceFromSyncSequence.swift rename to Sources/AsyncHTTPClient/AsyncAwait/AsyncLazySequence.swift From c04f6217b0b52c46730afe3291c64040eb6f81dd Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Tue, 11 Oct 2022 09:57:38 +0100 Subject: [PATCH 6/6] Fix review comments --- Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift | 6 +++--- .../AsyncAwait/SingleIteratorPrecondition.swift | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift index 7b291a93a..786b49eaf 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift @@ -90,7 +90,7 @@ extension HTTPClientResponse { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientResponse.Body { @usableFromInline enum Storage: Sendable { - case transaction(SingleIteratorPrecondition) + case transaction(TransactionBody) case anyAsyncSequence(AnyAsyncSequence) } } @@ -112,7 +112,7 @@ extension HTTPClientResponse.Body.Storage: AsyncSequence { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientResponse.Body.Storage { @usableFromInline enum AsyncIterator { - case transaction(SingleIteratorPrecondition.AsyncIterator) + case transaction(TransactionBody.AsyncIterator) case anyAsyncSequence(AnyAsyncSequence.AsyncIterator) } } @@ -133,7 +133,7 @@ extension HTTPClientResponse.Body.Storage.AsyncIterator: AsyncIteratorProtocol { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientResponse.Body { init(_ body: TransactionBody) { - self.init(.transaction(body.singleIteratorPrecondition)) + self.init(.transaction(body)) } @usableFromInline init(_ storage: Storage) { diff --git a/Sources/AsyncHTTPClient/AsyncAwait/SingleIteratorPrecondition.swift b/Sources/AsyncHTTPClient/AsyncAwait/SingleIteratorPrecondition.swift index 963fb1f66..04034db2d 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/SingleIteratorPrecondition.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/SingleIteratorPrecondition.swift @@ -14,7 +14,8 @@ import Atomics -/// Makes sure that a consumer of this `AsyncSequence` +/// Makes sure that a consumer of this `AsyncSequence` only calls `makeAsyncIterator()` at most once. +/// If `makeAsyncIterator()` is called multiple times, the program crashes. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @usableFromInline struct SingleIteratorPrecondition: AsyncSequence { @usableFromInline let base: Base @@ -26,7 +27,8 @@ import Atomics @inlinable func makeAsyncIterator() -> Base.AsyncIterator { precondition( - self.didCreateIterator.exchange(true, ordering: .relaxed) == false + self.didCreateIterator.exchange(true, ordering: .relaxed) == false, + "makeAsyncIterator() is only allowed to be called at most once." ) return self.base.makeAsyncIterator() }