Class: PumaPlus::WorkerThread::ChunkWriter

Inherits:
Object
  • Object
show all
Defined in:
lib/puma_plus/worker_thread.rb

Overview

The stream object handed to a Rack 3 streaming body.

The Rack SPEC requires read, write, <<, flush, close, close_read, close_write and closed?, and says their semantics "must be a best effort match to those of a normal Ruby IO or Socket object, using standard arguments and raising standard exceptions" (rack SPEC.rdoc:252). So this behaves like a Ruby IO opened for writing only -- including raising the same errors that one does, rather than inventing gentler ones.

Rack::Lint enforces only that the methods exist, which is how an earlier version passed every other check while omitting three of them and failing the moment a streaming body was actually used.

Instance Method Summary collapse

Constructor Details

#initialize(conn) ⇒ ChunkWriter

Returns a new instance of ChunkWriter.



227
228
229
230
# File 'lib/puma_plus/worker_thread.rb', line 227

def initialize(conn)
  @conn = conn
  @closed = false
end

Instance Method Details

#<<(chunk) ⇒ Object

IO#<< returns self so it can be chained, where #write returns a count. Aliasing the two would break stream << a << b.



249
250
251
252
# File 'lib/puma_plus/worker_thread.rb', line 249

def <<(chunk)
  write(chunk)
  self
end

#closeObject Also known as: close_write

Closing the stream ends the body but must NOT close the connection: the connection outlives the request and goes back to the idle pool. RESP_END is what terminates the body, and write_response sends it.



274
275
276
277
# File 'lib/puma_plus/worker_thread.rb', line 274

def close
  @closed = true
  nil
end

#close_readObject

Raises:

  • (IOError)


267
268
269
# File 'lib/puma_plus/worker_thread.rb', line 267

def close_read
  raise IOError, "closing non-duplex IO for reading"
end

#closed?Boolean

Returns:

  • (Boolean)


280
# File 'lib/puma_plus/worker_thread.rb', line 280

def closed? = @closed

#flushObject

Nothing is buffered on this side: every write is already a frame on the wire, which is the point (puma-dev batches SSE into one-second hiccups with a 1s FlushInterval). IO#flush returns self.



257
# File 'lib/puma_plus/worker_thread.rb', line 257

def flush = self

#readObject

Write-only, so reading raises exactly what a Ruby IO opened "w" raises. Full-duplex streaming would mean interleaving BODY_CHUNK reads with RESP_CHUNK writes on one connection, which the protocol's drain invariant (consume the body to BODY_END before RESP_END) forbids.

Raises:

  • (IOError)


263
264
265
# File 'lib/puma_plus/worker_thread.rb', line 263

def read(*)
  raise IOError, "not opened for reading"
end

#write(*chunks) ⇒ Object

IO#write takes any number of arguments and returns the total bytes.

Raises:

  • (IOError)


233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/puma_plus/worker_thread.rb', line 233

def write(*chunks)
  raise IOError, "closed stream" if @closed

  total = 0
  chunks.each do |chunk|
    data = chunk.to_s
    next if data.empty?

    @conn.write Wire.frame(Wire::RESP_CHUNK, data)
    total += data.bytesize
  end
  total
end