send() parks the handler coroutine when the per-stream queue is full. That is the right default for delivering every byte, but a producer that would rather drop a frame than accumulate lag — an SSE ticker, a metrics feed, a progress stream — has no way to ask. sendable() is not that way either: between the check and the send() another coroutine can fill the queue, and its false also covers closed, sealed and detached responses.
The pattern already exists in this extension: WebSocket::send() parks, WebSocket::trySend() refuses without queueing, and Room repeats the pair. Both read the same high-water mark, HttpServerConfig::setStreamWriteBufferBytes(), which is what h2_stream_sendable consults.
Proposal
HttpResponse::tryWrite(string $chunk): bool beside send(). False means the queue had no room: nothing queued, no header committed, so the same chunk can be offered later. A gone peer throws HttpException 499 rather than returning false — the two answers need opposite reactions, and trySend() on WebSocket already draws that line by throwing WebSocketClosedException.
One caveat belongs in the docs: a refused chunk is a slice of one byte stream, so dropping it corrupts the body. Only the framed dialects carry droppable units, and their twins (trySseEvent(), tryWriteMessage()) come with the rename step.
Blocked on one thing found while writing this: compressing_stream_ops has neither a sendable nor an is_alive slot, so under compression both questions answer from a NULL op — "always writable, never full".
Part of the response body contract in dev/PLAN.md.
send()parks the handler coroutine when the per-stream queue is full. That is the right default for delivering every byte, but a producer that would rather drop a frame than accumulate lag — an SSE ticker, a metrics feed, a progress stream — has no way to ask.sendable()is not that way either: between the check and thesend()another coroutine can fill the queue, and its false also covers closed, sealed and detached responses.The pattern already exists in this extension:
WebSocket::send()parks,WebSocket::trySend()refuses without queueing, andRoomrepeats the pair. Both read the same high-water mark,HttpServerConfig::setStreamWriteBufferBytes(), which is whath2_stream_sendableconsults.Proposal
HttpResponse::tryWrite(string $chunk): boolbesidesend(). False means the queue had no room: nothing queued, no header committed, so the same chunk can be offered later. A gone peer throwsHttpException499 rather than returning false — the two answers need opposite reactions, andtrySend()on WebSocket already draws that line by throwingWebSocketClosedException.One caveat belongs in the docs: a refused chunk is a slice of one byte stream, so dropping it corrupts the body. Only the framed dialects carry droppable units, and their twins (
trySseEvent(),tryWriteMessage()) come with the rename step.Blocked on one thing found while writing this:
compressing_stream_opshas neither asendablenor anis_aliveslot, so under compression both questions answer from a NULL op — "always writable, never full".Part of the response body contract in dev/PLAN.md.