@@ -32,14 +32,15 @@ extension HTTPClient {
32
32
/// A streaming uploader.
33
33
///
34
34
/// ``StreamWriter`` abstracts
35
- public struct StreamWriter {
36
- let closure : ( IOData ) -> EventLoopFuture < Void >
35
+ public struct StreamWriter : Sendable {
36
+ let closure : @ Sendable ( IOData ) -> EventLoopFuture < Void >
37
37
38
38
/// Create new ``HTTPClient/Body/StreamWriter``
39
39
///
40
40
/// - parameters:
41
41
/// - closure: function that will be called to write actual bytes to the channel.
42
- public init ( closure: @escaping ( IOData ) -> EventLoopFuture < Void > ) {
42
+ @preconcurrency
43
+ public init ( closure: @escaping @Sendable ( IOData ) -> EventLoopFuture < Void > ) {
43
44
self . closure = closure
44
45
}
45
46
@@ -55,15 +56,15 @@ extension HTTPClient {
55
56
func writeChunks< Bytes: Collection > (
56
57
of bytes: Bytes ,
57
58
maxChunkSize: Int
58
- ) -> EventLoopFuture < Void > where Bytes. Element == UInt8 {
59
- // `StreamWriter` is has design issues, for example
59
+ ) -> EventLoopFuture < Void > where Bytes. Element == UInt8 , Bytes : Sendable , Bytes . SubSequence : Sendable {
60
+ // `StreamWriter` has design issues, for example
60
61
// - https://github.com/swift-server/async-http-client/issues/194
61
62
// - https://github.com/swift-server/async-http-client/issues/264
62
63
// - We're not told the EventLoop the task runs on and the user is free to return whatever EL they
63
64
// want.
64
65
// One important consideration then is that we must lock around the iterator because we could be hopping
65
66
// between threads.
66
- typealias Iterator = EnumeratedSequence < ChunksOfCountCollection < Bytes > > . Iterator
67
+ typealias Iterator = BodyStreamIterator < Bytes >
67
68
typealias Chunk = ( offset: Int , element: ChunksOfCountCollection < Bytes > . Element )
68
69
69
70
func makeIteratorAndFirstChunk(
@@ -77,7 +78,7 @@ extension HTTPClient {
77
78
return nil
78
79
}
79
80
80
- return ( NIOLockedValueBox ( iterator) , chunk)
81
+ return ( NIOLockedValueBox ( BodyStreamIterator ( iterator) ) , chunk)
81
82
}
82
83
83
84
guard let ( iterator, chunk) = makeIteratorAndFirstChunk ( bytes: bytes) else {
@@ -86,20 +87,19 @@ extension HTTPClient {
86
87
87
88
@Sendable // can't use closure here as we recursively call ourselves which closures can't do
88
89
func writeNextChunk( _ chunk: Chunk , allDone: EventLoopPromise < Void > ) {
89
- if let nextElement = iterator. withLockedValue ( { $0. next ( ) } ) {
90
+ if let ( index , element ) = iterator. withLockedValue ( { $0. next ( ) } ) {
90
91
self . write ( . byteBuffer( ByteBuffer ( bytes: chunk. element) ) ) . map {
91
- let index = nextElement. offset
92
92
if ( index + 1 ) % 4 == 0 {
93
93
// Let's not stack-overflow if the futures insta-complete which they at least in HTTP/2
94
94
// mode.
95
95
// Also, we must frequently return to the EventLoop because we may get the pause signal
96
96
// from another thread. If we fail to do that promptly, we may balloon our body chunks
97
97
// into memory.
98
98
allDone. futureResult. eventLoop. execute {
99
- writeNextChunk ( nextElement , allDone: allDone)
99
+ writeNextChunk ( ( offset : index , element : element ) , allDone: allDone)
100
100
}
101
101
} else {
102
- writeNextChunk ( nextElement , allDone: allDone)
102
+ writeNextChunk ( ( offset : index , element : element ) , allDone: allDone)
103
103
}
104
104
} . cascadeFailure ( to: allDone)
105
105
} else {
@@ -188,7 +188,7 @@ extension HTTPClient {
188
188
@preconcurrency
189
189
@inlinable
190
190
public static func bytes< Bytes> ( _ bytes: Bytes ) -> Body
191
- where Bytes: RandomAccessCollection , Bytes: Sendable , Bytes. Element == UInt8 {
191
+ where Bytes: RandomAccessCollection , Bytes: Sendable , Bytes. SubSequence : Sendable , Bytes . Element == UInt8 {
192
192
Body ( contentLength: Int64 ( bytes. count) ) { writer in
193
193
if bytes. count <= bagOfBytesToByteBufferConversionChunkSize {
194
194
return writer. write ( . byteBuffer( ByteBuffer ( bytes: bytes) ) )
@@ -1080,3 +1080,26 @@ extension RequestBodyLength {
1080
1080
self = . known( length)
1081
1081
}
1082
1082
}
1083
+
1084
+ @usableFromInline
1085
+ struct BodyStreamIterator <
1086
+ Bytes: Collection
1087
+ > : IteratorProtocol , @unchecked Sendable where Bytes. Element == UInt8 , Bytes: Sendable {
1088
+ // @unchecked: swift-algorithms hasn't adopted Sendable yet. By inspection, the iterator
1089
+ // is safe to annotate as sendable.
1090
+ @usableFromInline
1091
+ typealias Element = ( offset: Int , element: Bytes . SubSequence )
1092
+
1093
+ @usableFromInline
1094
+ var _backing : EnumeratedSequence < ChunksOfCountCollection < Bytes > > . Iterator
1095
+
1096
+ @inlinable
1097
+ init ( _ backing: EnumeratedSequence < ChunksOfCountCollection < Bytes > > . Iterator ) {
1098
+ self . _backing = backing
1099
+ }
1100
+
1101
+ @inlinable
1102
+ mutating func next( ) -> Element ? {
1103
+ self . _backing. next ( )
1104
+ }
1105
+ }
0 commit comments