stream

package
v0.9.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

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

func Credential(r *http.Request) string

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

func FieldValue(value string) string

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.

func Headers

func Headers(header http.Header)

Headers sets what every server-sent event stream needs, and is where the reasons live.

Types

type Limits

type Limits struct {
	PerCredential int
	PerTenant     int
	PerProcess    int
}

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"
)

func (Refusal) String

func (r Refusal) String() string

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 NewRegistry(limits Limits) *Registry

func (*Registry) Admit

func (r *Registry) Admit(credential, tenant string) (Slot, Refusal)

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.

func (*Registry) Draining

func (r *Registry) Draining() bool

Draining reports whether the process has begun shutting down.

func (*Registry) Open

func (r *Registry) Open() int

Open is how many streams this process is holding. The gauge (§3.2), and what the health report reads.

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.

func (Slot) Release

func (s Slot) Release()

Release gives the slot back. Idempotent, and it must be called: a slot that outlives its connection is a stream this process will refuse to open for the rest of its life.

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

func (s *Writer) Comment(text string) error

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

func (s *Writer) Event(id, name, data string) error

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.

func (*Writer) Retry

func (s *Writer) Retry(milliseconds int) error

Retry tells a reconnecting client how long to wait, in milliseconds.

func (*Writer) Write

func (s *Writer) Write(frame string) error

Write puts one framed string on the wire and flushes it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL