1+ import AsyncAlgorithms
12import Hummingbird
23import Logging
34import NIOCore
@@ -18,7 +19,7 @@ public protocol AppArguments {
1819/// The request context can store any data that is associated with the request
1920/// This could include things like the authenticated user, a JWT token, or other information derived
2021/// from the request such as their IP address, or user agent.
21- ///
22+ ///
2223/// The Request Context can be accessed and modified by any middleware or route handler.
2324/// This allows middleware to pass information forward to the next middleware or route handler in the chain.
2425struct AppRequestContext : RequestContext {
@@ -86,41 +87,56 @@ func buildRouter(requestPublisher: Publisher<String>) -> Router<AppRequestContex
8687 // It will stay open until the client closes the connection, or the stream ends
8788 let allocator = ByteBufferAllocator ( )
8889
89- // A subscription to some data source is opened
90- // This might be a database like Redis, or some other data source
91- let ( stream, id) = requestPublisher. subscribe ( )
90+ // We create a stream to pass our cancel event to
91+ enum StreamResult : Sendable {
92+ case event( String )
93+ case cancel
94+ }
95+ let ( cancelStream, cancelCont) = AsyncStream . makeStream ( of: StreamResult . self)
9296
9397 // This is a helper that will call the `onGracefulShutdown` closure
9498 // when the application is shutting down.
9599 // This helps ensure that the application will gracefully shut down, meaning
96100 // any existing work will be correctly cleaned up before the application exits.
97101 try await withGracefulShutdownHandler {
98- // If connection if closed then this function will call the `onInboundCLosed ` closure
102+ // If connection if closed then this function will call the `onInboundClosed ` closure
99103 try await request. body. consumeWithInboundCloseHandler { requestBody in
100- // This loop will suspend until a new message is available, or the stream ends
101- // If the stream ends, the loop will finish exiting the loop.
102- for try await value in stream {
103- // A new value was received from the data source
104- // We create a new ServerSentEvent with the value and write it to the response body
105- // The `await` before the `write` is used to ensure that the write is completed
106- // before the loop continues to await the next value
107- // This applies backpressure to the data source
108- // Depending on the implementation, the data source could buffer the messages
109- // in memory or suspend the production of events until the client is ready to receive them
110- // Additionally, data could be dropped if the client is unable to keep up with the rate of data production
111- try await writer. write (
112- ServerSentEvent ( data: . init( string: value) ) . makeBuffer (
113- allocator: allocator
114- )
115- )
104+
105+ // A subscription to some data source is opened
106+ // This might be a database like Redis, or some other data source
107+ try await requestPublisher. subscribe { stream in
108+ // We merge the publisher stream with the cancellation stream
109+ let publishAndCancelStream = merge ( cancelStream, stream. map { . event( $0) } )
110+
111+ // This will wait for either a publish event or a cancellation event
112+ outsideLoop: for try await value in publishAndCancelStream {
113+ // A new value was received from the data source
114+ switch value {
115+ case . event( let value) :
116+ // if it is a publish event we create a new ServerSentEvent with the value
117+ // and write it to the response body
118+ // The `await` before the `write` is used to ensure that the write is completed
119+ // before the loop continues to await the next value
120+ // This applies backpressure to the data source
121+ // Depending on the implementation, the data source could buffer the messages
122+ // in memory or suspend the production of events until the client is ready to receive them
123+ // Additionally, data could be dropped if the client is unable to keep up with the rate of data production
124+ try await writer. write (
125+ ServerSentEvent ( data: . init( string: value) ) . makeBuffer (
126+ allocator: allocator
127+ )
128+ )
129+ case . cancel:
130+ // if it is a cancellation event then we exit the loop
131+ break outsideLoop
132+ }
133+ }
116134 }
117135 } onInboundClosed: {
118- // If the client closes the connection, we unsubscribe from the data source
119- requestPublisher. unsubscribe ( id)
136+ cancelCont. yield ( . cancel)
120137 }
121138 } onGracefulShutdown: {
122- // If the application is shutting down, we unsubscribe from the data source
123- requestPublisher. unsubscribe ( id)
139+ cancelCont. yield ( . cancel)
124140 }
125141 try await writer. finish ( nil )
126142 }
0 commit comments