Documentation
¶
Overview ¶
Package stream is the machinery a long-lived connection needs, shared by every adapter that holds one.
It exists because there is more than one: `GET /stream` serves changes to a browser, and `GET /mcp` serves notifications to an agent. **An agent's stream is not a different kind of connection from a browser's and must not have a different kind of limit** - so the caps, the draining and the refusal reasons live here rather than in either adapter, and a pod's capacity is one number whoever is holding it.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Credential ¶
Credential is the per-credential key a connection is counted under.
Here rather than in either adapter, and that is not tidiness: the per-credential cap only means anything if both streams key a credential the same way. An agent and a browser presenting the same token have to land in the same counter, or a client could double its allowance by opening half its connections at the other endpoint.
A fingerprint rather than the credential itself, for the rate limiter's reason: the map ends up in a heap dump, and a heap dump with live tokens in it is a second incident on top of the first (rule 10, security.md §9). Plain SHA-256 without a pepper is enough - the value never leaves the process and is never compared against anything stored, so the only property that matters is that two different tokens land in two different counters.
A request with no bearer credential answers empty, which the registry reads as "do not count this one per credential". That is the right answer rather than a hole: every stream in this product is behind authentication, so an empty key means a shape the caller has already refused.
func FieldValue ¶
FieldValue keeps a value on one line.
A newline in an event's name, an identifier or a comment's text would end the field and let whatever follows be read as the next one - the same shape as a header injection, in a protocol whose separator is also a line break.
Types ¶
type Limits ¶
Limits is how many streams may be open at once, at each of the three levels a long-lived connection has to be bounded at.
Three rather than one, because they answer three different failures. Per credential stops one client reconnecting in a loop and holding a hundred sockets it never reads. Per tenant stops one busy workspace taking a whole pod's capacity from everybody else on it. Per process is the load shedding of observability-reliability.md §6 applied to the resource a stream actually consumes: above the threshold, new connections are refused with `503` and a `Retry-After` before latency tips over for everyone - the connections already open are never dropped to make room, because shedding is about not accepting more work, not about abandoning work in hand.
type Refusal ¶
type Refusal string
Refusal says which limit was reached, for the problem document and the metric. A closed set, because it is a label (observability-reliability.md §3.2).
const ( // RefusedNone means the slot was granted. RefusedNone Refusal = "" // RefusedCredential is one client holding too many. RefusedCredential Refusal = "credential" // RefusedTenant is one workspace holding too many. RefusedTenant Refusal = "tenant" // RefusedProcess is this pod holding too many, whoever they belong to. RefusedProcess Refusal = "process" // RefusedDraining is the process shutting down. RefusedDraining Refusal = "draining" )
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry counts what is open and hands out the right to open one more.
Process-local, and deliberately so. The `api` role is stateless and horizontally scaled, so there is no shared count to keep - a limit per process is a limit per pod, which is exactly the resource being protected. A cluster-wide cap would need coordination on the request path for a number nobody can act on anyway.
func NewRegistry ¶
func (*Registry) Admit ¶
Admit grants the right to open one stream, or says which limit refused it.
The keys are opaque to this type: whoever calls it decides what "one credential" means, and it is a fingerprint rather than the credential itself - the same reasoning that keeps the rate limiter's bucket key a hash (security.md §9, rule 10).
func (*Registry) CloseAll ¶
func (r *Registry) CloseAll()
CloseAll asks every open stream to end, and refuses new ones from now on.
Called on `SIGTERM`, before the HTTP server is asked to shut down. Without it `Shutdown` would wait out the whole grace period on connections that are working exactly as designed - a stream has no natural end, so nothing but this tells it there is one (observability-reliability.md §9).
It does not wait. The handlers return on their own and the server's own drain is what waits for them, which is the one place that already knows how long it may.
type Slot ¶
type Slot struct {
// Closing is closed when the process wants the stream to end. The handler selects on it beside
// the client's own context, so a shutdown ends the connection the same way a client leaving
// does - by returning from the handler, which is what lets the server drain (§9).
Closing <-chan struct{}
// contains filtered or unexported fields
}
Slot is the right to hold one stream open.
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
Writer frames server-sent events onto a response, and flushes each one.
The flush is the whole reason this type exists rather than a few Fprintf calls. net/http buffers a response, so an event that was written and not flushed is an event the client gets when the next four kilobytes arrive - which for a quiet workspace is minutes later, and which makes a stream indistinguishable from a broken one.
Here rather than in either adapter because both write the same wire format: `GET /stream` carries change records to a browser and `GET /mcp` carries JSON-RPC notifications to an agent, and the framing - the field names, the blank line, the flush, the newline stripping - is the same protocol in both. What differs is what goes in the `data`, which is each adapter's own.
func NewWriter ¶
func NewWriter(w http.ResponseWriter) *Writer
NewWriter takes the response over. It writes no header: the caller decides the status and the content type, because a stream that has begun cannot become a problem document.
func (*Writer) Comment ¶
Comment writes a `:` line: the heartbeat, and the goodbye.
A comment rather than an event, because a client must be able to ignore it without knowing what it is - the specification says a line beginning with a colon is discarded, which is exactly the contract a keep-alive needs.
func (*Writer) Event ¶
Event writes one frame. An empty id or name leaves that field out, which is legal and is what a notification with no identity wants.