77
88#if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) || os(visionOS)
99
10+ import func Foundation. CFWriteStreamSetDispatchQueue
1011import class Foundation. DispatchQueue
11- import func Foundation. autoreleasepool
1212import class Foundation. NSObject
1313import class Foundation. Stream
1414import class Foundation. InputStream
1515import class Foundation. OutputStream
1616import class Foundation. Thread
1717import class Foundation. RunLoop
18+ import class Foundation. Timer
19+ import struct Foundation. TimeInterval
1820import protocol Foundation. StreamDelegate
1921
2022/// Reads data from a smithy-swift native `ReadableStream` and streams the data to a Foundation `InputStream`.
@@ -39,35 +41,41 @@ class FoundationStreamBridge: NSObject, StreamDelegate {
3941 /// A Foundation `OutputStream` that will read from the `ReadableStream`
4042 private let outputStream : OutputStream
4143
42- /// Actor used to isolate the stream status from multiple concurrent accesses.
43- actor ReadableStreamStatus {
44+ /// A Logger for logging events.
45+ private let logger : LogAgent
46+
47+ /// Actor used to ensure writes are performed in series.
48+ actor WriteCoordinator {
49+ var task : Task < Void , Error > ?
4450
4551 /// `true` if the readable stream has been found to be empty, `false` otherwise. Will flip to `true` if the readable stream is read,
4652 /// and `nil` is returned.
47- var isEmpty = false
53+ var readableStreamIsEmpty = false
4854
4955 /// Sets stream status to indicate the stream is empty.
50- func setIsEmpty ( ) async {
51- isEmpty = true
56+ func setReadableStreamIsEmpty ( ) async {
57+ readableStreamIsEmpty = true
5258 }
53- }
5459
55- /// Actor used to isolate the stream status from multiple concurrent accesses.
56- private var readableStreamStatus = ReadableStreamStatus ( )
60+ /// Creates a new concurrent Task that executes the passed block, ensuring that the previous Task
61+ /// finishes before this task starts.
62+ ///
63+ /// Acts as a sort of "serial queue" of Swift concurrency tasks.
64+ /// - Parameter block: The code to be performed in this task.
65+ func perform( _ block: @escaping @Sendable ( WriteCoordinator) async throws -> Void ) {
66+ self . task = Task { [ task] in
67+ _ = await task? . result
68+ try await block ( self )
69+ }
70+ }
71+ }
5772
58- /// A shared serial DispatchQueue to run the `perform`-on-thread operations.
59- /// Performing thread operations on an async queue allows Swift concurrency tasks to not block.
60- private static let queue = DispatchQueue ( label: " AWSFoundationStreamBridge " )
73+ /// Actor used to enforce the order of multiple concurrent stream writes.
74+ private let writeCoordinator = WriteCoordinator ( )
6175
62- /// Foundation Streams require a run loop on which to post callbacks for their delegates.
63- /// All stream operations should be performed on the same thread as the delegate callbacks.
64- /// A single shared `Thread` is started and is used to host the RunLoop for all Foundation Stream callbacks.
65- private static let thread : Thread = {
66- let thread = Thread { autoreleasepool { RunLoop . current. run ( ) } }
67- thread. name = " AWSFoundationStreamBridge "
68- thread. start ( )
69- return thread
70- } ( )
76+ /// A shared serial DispatchQueue to run the stream operations.
77+ /// Performing operations on an async queue allows Swift concurrency tasks to not block.
78+ private let queue = DispatchQueue ( label: " AWSFoundationStreamBridge " )
7179
7280 // MARK: - init & deinit
7381
@@ -78,8 +86,8 @@ class FoundationStreamBridge: NSObject, StreamDelegate {
7886 /// - Parameters:
7987 /// - readableStream: The `ReadableStream` that serves as the input to the bridge.
8088 /// - bufferSize: The number of bytes in the in-memory buffer. The buffer is allocated for this size no matter if in use or not.
81- /// Defaults to 4096 bytes.
82- init ( readableStream: ReadableStream , bufferSize: Int = 4096 ) {
89+ /// Defaults to 65536 bytes.
90+ init ( readableStream: ReadableStream , bufferSize: Int = 65_536 , logger : LogAgent ) {
8391 var inputStream : InputStream ?
8492 var outputStream : OutputStream ?
8593
@@ -98,77 +106,73 @@ class FoundationStreamBridge: NSObject, StreamDelegate {
98106 self . readableStream = readableStream
99107 self . inputStream = inputStream
100108 self . outputStream = outputStream
109+ self . logger = logger
110+
111+ // The stream is configured to deliver its callbacks on the dispatch queue.
112+ // This precludes the need for a Thread with RunLoop.
113+ CFWriteStreamSetDispatchQueue ( outputStream, queue)
101114 }
102115
103116 // MARK: - Opening & closing
104117
105- /// Schedule the output stream on the special thread reserved for stream callbacks.
118+ /// Schedule the output stream on the queue for stream callbacks.
106119 /// Do not wait to complete opening before returning.
107120 func open( ) async {
108121 await withCheckedContinuation { continuation in
109- Self . queue. async {
110- self . perform ( #selector( self . openOnThread) , on: Self . thread, with: nil , waitUntilDone: false )
122+ queue. async {
123+ self . outputStream. delegate = self
124+ self . outputStream. open ( )
125+ continuation. resume ( )
111126 }
112- continuation. resume ( )
113127 }
114128 }
115129
116- /// Configure the output stream to make StreamDelegate callback to this bridge using the special thread / run loop, and open the output stream.
117- /// The input stream is not included here. It will be configured by `URLSession` when the HTTP request is initiated.
118- @objc private func openOnThread( ) {
119- outputStream. delegate = self
120- outputStream. schedule ( in: RunLoop . current, forMode: . default)
121- outputStream. open ( )
122- }
123-
124130 /// Unschedule the output stream on the special stream callback thread.
125131 /// Do not wait to complete closing before returning.
126132 func close( ) async {
127133 await withCheckedContinuation { continuation in
128- Self . queue. async {
129- self . perform ( #selector( self . closeOnThread) , on: Self . thread, with: nil , waitUntilDone: false )
134+ queue. async {
135+ self . outputStream. close ( )
136+ self . outputStream. delegate = nil
137+ continuation. resume ( )
130138 }
131- continuation. resume ( )
132139 }
133140 }
134141
135- /// Close the output stream and remove it from the thread / run loop.
136- @objc private func closeOnThread( ) {
137- outputStream. close ( )
138- outputStream. remove ( from: RunLoop . current, forMode: . default)
139- outputStream. delegate = nil
140- }
141-
142142 // MARK: - Writing to bridge
143143
144144 /// Tries to read from the readable stream if possible, then transfer the data to the output stream.
145145 private func writeToOutput( ) async throws {
146- var data = Data ( )
147- if await !readableStreamStatus . isEmpty {
148- if let newData = try await readableStream. readAsync ( upToCount: bufferSize) {
149- data = newData
150- } else {
151- await readableStreamStatus. setIsEmpty ( )
152- await close ( )
146+ await writeCoordinator. perform { [ self ] writeCoordinator in
147+ var data = Data ( )
148+ if await !writeCoordinator . readableStreamIsEmpty {
149+ if let newData = try await readableStream. readAsync ( upToCount: bufferSize) {
150+ data = newData
151+ } else {
152+ await writeCoordinator. setReadableStreamIsEmpty ( )
153+ await close ( )
154+ }
153155 }
156+ try await writeToOutputStream( data: data)
154157 }
155- try await writeToOutputStream( data: data)
156- }
157-
158- private class WriteToOutputStreamResult : NSObject {
159- var data = Data ( )
160- var error : Error ?
161158 }
162159
163160 /// Write the passed data to the output stream, using the reserved thread.
164161 private func writeToOutputStream( data: Data ) async throws {
165162 try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation < Void , Error > ) in
166- Self . queue. async {
167- let result = WriteToOutputStreamResult ( )
168- result. data = data
169- let selector = #selector( self . writeToOutputStreamOnThread)
170- self . perform ( selector, on: Self . thread, with: result, waitUntilDone: true )
171- if let error = result. error {
163+ queue. async { [ self ] in
164+ guard !buffer. isEmpty || !data. isEmpty else { continuation. resume ( ) ; return }
165+ buffer. append ( data)
166+ var writeCount = 0
167+ buffer. withUnsafeBytes { bufferPtr in
168+ guard let bytePtr = bufferPtr. bindMemory ( to: UInt8 . self) . baseAddress else { return }
169+ writeCount = outputStream. write ( bytePtr, maxLength: buffer. count)
170+ }
171+ if writeCount > 0 {
172+ logger. info ( " FoundationStreamBridge: wrote \( writeCount) bytes to request body " )
173+ buffer. removeFirst ( writeCount)
174+ }
175+ if let error = outputStream. streamError {
172176 continuation. resume ( throwing: error)
173177 } else {
174178 continuation. resume ( )
@@ -177,34 +181,28 @@ class FoundationStreamBridge: NSObject, StreamDelegate {
177181 }
178182 }
179183
180- /// Append the new data to the buffer, then write to the output stream. Return any error to the caller using the param object.
181- @objc private func writeToOutputStreamOnThread( _ result: WriteToOutputStreamResult ) {
182- guard !buffer. isEmpty || !result. data. isEmpty else { return }
183- buffer. append ( result. data)
184- var writeCount = 0
185- buffer. withUnsafeBytes { bufferPtr in
186- let bytePtr = bufferPtr. bindMemory ( to: UInt8 . self) . baseAddress!
187- writeCount = outputStream. write ( bytePtr, maxLength: buffer. count)
188- }
189- if writeCount > 0 {
190- buffer. removeFirst ( writeCount)
191- }
192- result. error = outputStream. streamError
193- }
194-
195184 // MARK: - StreamDelegate protocol
196185
197- /// The stream places this callback when appropriate. Call will be delivered on the special thread / run loop for stream callbacks.
186+ /// The stream places this callback when appropriate. Call will be delivered on the GCD queue for stream callbacks.
198187 /// `.hasSpaceAvailable` prompts this type to query the readable stream for more data.
199188 @objc func stream( _ aStream: Foundation . Stream , handle eventCode: Foundation . Stream . Event ) {
200189 switch eventCode {
190+ case . openCompleted:
191+ break
192+ case . hasBytesAvailable:
193+ break
201194 case . hasSpaceAvailable:
202195 // Since space is available, try and read from the ReadableStream and
203196 // transfer the data to the Foundation stream pair.
204197 // Use a `Task` to perform the operation within Swift concurrency.
205198 Task { try await writeToOutput ( ) }
206- default :
199+ case . errorOccurred:
200+ logger. info ( " FoundationStreamBridge: .errorOccurred event " )
201+ logger. info ( " FoundationStreamBridge: Stream error: \( String ( describing: aStream. streamError) ) " )
202+ case . endEncountered:
207203 break
204+ default :
205+ logger. info ( " FoundationStreamBridge: Other stream event occurred: \( eventCode) " )
208206 }
209207 }
210208}
0 commit comments