Skip to content

Commit d7b69d9

Browse files
authored
Make HTTPClientResponse.init public (#632)
1 parent f17a47e commit d7b69d9

File tree

9 files changed

+310
-97
lines changed

9 files changed

+310
-97
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
@usableFromInline
16+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
17+
struct AnyAsyncSequence<Element>: Sendable, AsyncSequence {
18+
@usableFromInline typealias AsyncIteratorNextCallback = () async throws -> Element?
19+
20+
@usableFromInline struct AsyncIterator: AsyncIteratorProtocol {
21+
@usableFromInline let nextCallback: AsyncIteratorNextCallback
22+
23+
@inlinable init(nextCallback: @escaping AsyncIteratorNextCallback) {
24+
self.nextCallback = nextCallback
25+
}
26+
27+
@inlinable mutating func next() async throws -> Element? {
28+
try await self.nextCallback()
29+
}
30+
}
31+
32+
@usableFromInline var makeAsyncIteratorCallback: @Sendable () -> AsyncIteratorNextCallback
33+
34+
@inlinable init<SequenceOfBytes>(
35+
_ asyncSequence: SequenceOfBytes
36+
) where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == Element {
37+
self.makeAsyncIteratorCallback = {
38+
var iterator = asyncSequence.makeAsyncIterator()
39+
return {
40+
try await iterator.next()
41+
}
42+
}
43+
}
44+
45+
@inlinable func makeAsyncIterator() -> AsyncIterator {
46+
.init(nextCallback: self.makeAsyncIteratorCallback())
47+
}
48+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
16+
@usableFromInline
17+
struct AsyncLazySequence<Base: Sequence>: AsyncSequence {
18+
@usableFromInline typealias Element = Base.Element
19+
@usableFromInline struct AsyncIterator: AsyncIteratorProtocol {
20+
@usableFromInline var iterator: Base.Iterator
21+
@inlinable init(iterator: Base.Iterator) {
22+
self.iterator = iterator
23+
}
24+
25+
@inlinable mutating func next() async throws -> Base.Element? {
26+
self.iterator.next()
27+
}
28+
}
29+
30+
@usableFromInline var base: Base
31+
32+
@inlinable init(base: Base) {
33+
self.base = base
34+
}
35+
36+
@inlinable func makeAsyncIterator() -> AsyncIterator {
37+
.init(iterator: self.base.makeIterator())
38+
}
39+
}
40+
41+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
42+
extension AsyncLazySequence: Sendable where Base: Sendable {}
43+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
44+
extension AsyncLazySequence.AsyncIterator: Sendable where Base.Iterator: Sendable {}
45+
46+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
47+
extension Sequence {
48+
/// Turns `self` into an `AsyncSequence` by vending each element of `self` asynchronously.
49+
@inlinable var async: AsyncLazySequence<Self> {
50+
.init(base: self)
51+
}
52+
}

Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift

Lines changed: 83 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -33,98 +33,125 @@ public struct HTTPClientResponse: Sendable {
3333
/// The body of this HTTP response.
3434
public var body: Body
3535

36-
/// A representation of the response body for an HTTP response.
37-
///
38-
/// The body is streamed as an `AsyncSequence` of `ByteBuffer`, where each `ByteBuffer` contains
39-
/// an arbitrarily large chunk of data. The boundaries between `ByteBuffer` objects in the sequence
40-
/// are entirely synthetic and have no semantic meaning.
41-
public struct Body: Sendable {
42-
private let bag: Transaction
43-
private let reference: ResponseRef
44-
45-
fileprivate init(_ transaction: Transaction) {
46-
self.bag = transaction
47-
self.reference = ResponseRef(transaction: transaction)
48-
}
49-
}
50-
5136
init(
5237
bag: Transaction,
5338
version: HTTPVersion,
5439
status: HTTPResponseStatus,
5540
headers: HTTPHeaders
5641
) {
57-
self.body = Body(bag)
5842
self.version = version
5943
self.status = status
6044
self.headers = headers
45+
self.body = Body(TransactionBody(bag))
46+
}
47+
48+
@inlinable public init(
49+
version: HTTPVersion = .http1_1,
50+
status: HTTPResponseStatus = .ok,
51+
headers: HTTPHeaders = [:],
52+
body: Body = Body()
53+
) {
54+
self.version = version
55+
self.status = status
56+
self.headers = headers
57+
self.body = body
6158
}
6259
}
6360

6461
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
65-
extension HTTPClientResponse.Body: AsyncSequence {
66-
public typealias Element = AsyncIterator.Element
62+
extension HTTPClientResponse {
63+
/// A representation of the response body for an HTTP response.
64+
///
65+
/// The body is streamed as an `AsyncSequence` of `ByteBuffer`, where each `ByteBuffer` contains
66+
/// an arbitrarily large chunk of data. The boundaries between `ByteBuffer` objects in the sequence
67+
/// are entirely synthetic and have no semantic meaning.
68+
public struct Body: AsyncSequence, Sendable {
69+
public typealias Element = ByteBuffer
70+
public struct AsyncIterator: AsyncIteratorProtocol {
71+
@usableFromInline var storage: Storage.AsyncIterator
6772

68-
public struct AsyncIterator: AsyncIteratorProtocol {
69-
private let stream: IteratorStream
73+
@inlinable init(storage: Storage.AsyncIterator) {
74+
self.storage = storage
75+
}
7076

71-
fileprivate init(stream: IteratorStream) {
72-
self.stream = stream
77+
@inlinable public mutating func next() async throws -> ByteBuffer? {
78+
try await self.storage.next()
79+
}
7380
}
7481

75-
public mutating func next() async throws -> ByteBuffer? {
76-
try await self.stream.next()
82+
@usableFromInline var storage: Storage
83+
84+
@inlinable public func makeAsyncIterator() -> AsyncIterator {
85+
.init(storage: self.storage.makeAsyncIterator())
7786
}
7887
}
88+
}
7989

80-
public func makeAsyncIterator() -> AsyncIterator {
81-
AsyncIterator(stream: IteratorStream(bag: self.bag))
90+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
91+
extension HTTPClientResponse.Body {
92+
@usableFromInline enum Storage: Sendable {
93+
case transaction(TransactionBody)
94+
case anyAsyncSequence(AnyAsyncSequence<ByteBuffer>)
8295
}
8396
}
8497

8598
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
86-
extension HTTPClientResponse.Body {
87-
/// The purpose of this object is to inform the transaction about the response body being deinitialized.
88-
/// If the users has not called `makeAsyncIterator` on the body, before it is deinited, the http
89-
/// request needs to be cancelled.
90-
fileprivate final class ResponseRef: Sendable {
91-
private let transaction: Transaction
92-
93-
init(transaction: Transaction) {
94-
self.transaction = transaction
99+
extension HTTPClientResponse.Body.Storage: AsyncSequence {
100+
@usableFromInline typealias Element = ByteBuffer
101+
102+
@inlinable func makeAsyncIterator() -> AsyncIterator {
103+
switch self {
104+
case .transaction(let transaction):
105+
return .transaction(transaction.makeAsyncIterator())
106+
case .anyAsyncSequence(let anyAsyncSequence):
107+
return .anyAsyncSequence(anyAsyncSequence.makeAsyncIterator())
95108
}
109+
}
110+
}
111+
112+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
113+
extension HTTPClientResponse.Body.Storage {
114+
@usableFromInline enum AsyncIterator {
115+
case transaction(TransactionBody.AsyncIterator)
116+
case anyAsyncSequence(AnyAsyncSequence<ByteBuffer>.AsyncIterator)
117+
}
118+
}
96119

97-
deinit {
98-
self.transaction.responseBodyDeinited()
120+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
121+
extension HTTPClientResponse.Body.Storage.AsyncIterator: AsyncIteratorProtocol {
122+
@inlinable mutating func next() async throws -> ByteBuffer? {
123+
switch self {
124+
case .transaction(let iterator):
125+
return try await iterator.next()
126+
case .anyAsyncSequence(var iterator):
127+
defer { self = .anyAsyncSequence(iterator) }
128+
return try await iterator.next()
99129
}
100130
}
101131
}
102132

103133
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
104134
extension HTTPClientResponse.Body {
105-
internal class IteratorStream {
106-
struct ID: Hashable {
107-
private let objectID: ObjectIdentifier
108-
109-
init(_ object: IteratorStream) {
110-
self.objectID = ObjectIdentifier(object)
111-
}
112-
}
135+
init(_ body: TransactionBody) {
136+
self.init(.transaction(body))
137+
}
113138

114-
private var id: ID { ID(self) }
115-
private let bag: Transaction
139+
@usableFromInline init(_ storage: Storage) {
140+
self.storage = storage
141+
}
116142

117-
init(bag: Transaction) {
118-
self.bag = bag
119-
}
143+
public init() {
144+
self = .stream(EmptyCollection<ByteBuffer>().async)
145+
}
120146

121-
deinit {
122-
self.bag.responseBodyIteratorDeinited(streamID: self.id)
123-
}
147+
@inlinable public static func stream<SequenceOfBytes>(
148+
_ sequenceOfBytes: SequenceOfBytes
149+
) -> Self where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == ByteBuffer {
150+
self.init(.anyAsyncSequence(AnyAsyncSequence(sequenceOfBytes.singleIteratorPrecondition)))
151+
}
124152

125-
func next() async throws -> ByteBuffer? {
126-
try await self.bag.nextResponsePart(streamID: self.id)
127-
}
153+
public static func bytes(_ byteBuffer: ByteBuffer) -> Self {
154+
.stream(CollectionOfOne(byteBuffer).async)
128155
}
129156
}
130157

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Atomics
16+
17+
/// Makes sure that a consumer of this `AsyncSequence` only calls `makeAsyncIterator()` at most once.
18+
/// If `makeAsyncIterator()` is called multiple times, the program crashes.
19+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
20+
@usableFromInline struct SingleIteratorPrecondition<Base: AsyncSequence>: AsyncSequence {
21+
@usableFromInline let base: Base
22+
@usableFromInline let didCreateIterator: ManagedAtomic<Bool> = .init(false)
23+
@usableFromInline typealias Element = Base.Element
24+
@inlinable init(base: Base) {
25+
self.base = base
26+
}
27+
28+
@inlinable func makeAsyncIterator() -> Base.AsyncIterator {
29+
precondition(
30+
self.didCreateIterator.exchange(true, ordering: .relaxed) == false,
31+
"makeAsyncIterator() is only allowed to be called at most once."
32+
)
33+
return self.base.makeAsyncIterator()
34+
}
35+
}
36+
37+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
38+
extension SingleIteratorPrecondition: @unchecked Sendable where Base: Sendable {}
39+
40+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
41+
extension AsyncSequence {
42+
@inlinable var singleIteratorPrecondition: SingleIteratorPrecondition<Self> {
43+
.init(base: self)
44+
}
45+
}

Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ extension Transaction {
3030
case queued(CheckedContinuation<HTTPClientResponse, Error>, HTTPRequestScheduler)
3131
case deadlineExceededWhileQueued(CheckedContinuation<HTTPClientResponse, Error>)
3232
case executing(ExecutionContext, RequestStreamState, ResponseStreamState)
33-
case finished(error: Error?, HTTPClientResponse.Body.IteratorStream.ID?)
33+
case finished(error: Error?, TransactionBody.AsyncIterator.ID?)
3434
}
3535

3636
fileprivate enum RequestStreamState {
@@ -52,9 +52,9 @@ extension Transaction {
5252
// We are waiting for the user to create a response body iterator and to call next on
5353
// it for the first time.
5454
case waitingForResponseIterator(CircularBuffer<ByteBuffer>, next: Next)
55-
case buffering(HTTPClientResponse.Body.IteratorStream.ID, CircularBuffer<ByteBuffer>, next: Next)
56-
case waitingForRemote(HTTPClientResponse.Body.IteratorStream.ID, CheckedContinuation<ByteBuffer?, Error>)
57-
case finished(HTTPClientResponse.Body.IteratorStream.ID, CheckedContinuation<ByteBuffer?, Error>)
55+
case buffering(TransactionBody.AsyncIterator.ID, CircularBuffer<ByteBuffer>, next: Next)
56+
case waitingForRemote(TransactionBody.AsyncIterator.ID, CheckedContinuation<ByteBuffer?, Error>)
57+
case finished(TransactionBody.AsyncIterator.ID, CheckedContinuation<ByteBuffer?, Error>)
5858
}
5959

6060
private var state: State
@@ -510,7 +510,7 @@ extension Transaction {
510510
}
511511
}
512512

513-
mutating func responseBodyIteratorDeinited(streamID: HTTPClientResponse.Body.IteratorStream.ID) -> FailAction {
513+
mutating func responseBodyIteratorDeinited(streamID: TransactionBody.AsyncIterator.ID) -> FailAction {
514514
switch self.state {
515515
case .initialized, .queued, .deadlineExceededWhileQueued, .executing(_, _, .waitingForResponseHead):
516516
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 {
536536
}
537537

538538
mutating func consumeNextResponsePart(
539-
streamID: HTTPClientResponse.Body.IteratorStream.ID,
539+
streamID: TransactionBody.AsyncIterator.ID,
540540
continuation: CheckedContinuation<ByteBuffer?, Error>
541541
) -> ConsumeAction {
542542
switch self.state {
@@ -639,8 +639,8 @@ extension Transaction {
639639
}
640640

641641
private func verifyStreamIDIsEqual(
642-
registered: HTTPClientResponse.Body.IteratorStream.ID,
643-
this: HTTPClientResponse.Body.IteratorStream.ID,
642+
registered: TransactionBody.AsyncIterator.ID,
643+
this: TransactionBody.AsyncIterator.ID,
644644
file: StaticString = #file,
645645
line: UInt = #line
646646
) {

0 commit comments

Comments
 (0)