Class: Insika::Server::SSEBody
- Inherits:
-
Object
- Object
- Insika::Server::SSEBody
- Defined in:
- lib/insika/server/sse_body.rb
Overview
SSE response body (evolves SSEStream). SSEStream
received a producer block (the Runner wrote into it); SSEBody DRAINS a
Subscription from the EventStream: each subscriber has its
own queue; #each blocks the CONSUMER's fiber until the subscription
closes. The wire is EXACTLY Event#to_h.
Constant Summary collapse
- PING =
SSE comment — doesn't pollute the consumer
": ping\n\n"- DEFAULT_SERIALIZE =
serialize: maps an Event -> String (SSE frame) OR nil (discarded event, no frame). Default = the canonical wire
data: <Event#to_h>. The /v1/responses adapter injects a serializer that produces OpenAI Responses events (and skips those with no counterpart). ->(event) { "data: #{JSON.generate(event.to_h)}\n\n" }
Instance Method Summary collapse
-
#call(stream) ⇒ Object
Rack 3 STREAMING BODY (
#call(stream)), NOT#each. -
#initialize(subscription:, heartbeat: 15, serialize: nil) ⇒ SSEBody
constructor
subscription: any object with #each (yields Events) and #close.
Constructor Details
#initialize(subscription:, heartbeat: 15, serialize: nil) ⇒ SSEBody
subscription: any object with #each (yields Events) and #close. heartbeat: seconds of silence before emitting a ping (15s clears 60s ALB/nginx idle timeouts with room to spare).
27 28 29 30 31 |
# File 'lib/insika/server/sse_body.rb', line 27 def initialize(subscription:, heartbeat: 15, serialize: nil) @subscription = subscription @heartbeat = heartbeat @serialize = serialize || DEFAULT_SERIALIZE end |
Instance Method Details
#call(stream) ⇒ Object
Rack 3 STREAMING BODY (#call(stream)), NOT #each. Under
protocol-rack/protocol-http1 (the stack of Async::HTTP::Server AND Falcon),
a body that responds to #each is routed to Body::Enumerable, whose
read runs the #each in a PLAIN Enumerator Fiber (not an Async::Task) —
there Async::Task.current raises "No async task available", the loop died
swallowed in the rescue and the body came out EMPTY. By exposing #call (and NOT #each),
the body is routed to Body::Streaming, which schedules the block via
Fiber.schedule under the reactor's scheduler — so the subscription drains and the
frames actually reach the socket (incrementally).
The stream (Protocol::HTTP::Body::Stream) responds to #write/#close. Drains the
subscription DIRECTLY (no Async::Task.current): when this fiber blocks
waiting for the next event, the scheduler runs the writer, which pushes the
already-written frame to the socket. Heartbeat via Timeout.timeout (scheduler hook),
which works in the scheduled fiber — keeps the connection alive while idle (L4).
48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/insika/server/sse_body.rb', line 48 def call(stream) drain(stream) rescue StandardError # Client disconnected: `stream.write` raises when the socket closes. # No exception escapes; the turn's task is NEVER cancelled here — the # execution belongs to the runtime, not the connection (reconnect at /v1/events). nil ensure @subscription.close stream.close end |