Documentation
¶
Overview ¶
Package livewire synchronises the result of a query over one WebSocket: a client subscribes to a question, receives its answer, then every answer after it.
This is the Go implementation of the contract in packages/protocol/SPEC.md. That document is normative — where this code and the specification disagree, the specification is right and this is a bug.
Index ¶
Constants ¶
const ( SubscribeEvent = "subscribe" UnsubscribeEvent = "unsubscribe" UpdateEvent = "update" )
Frame names are the vocabulary of the protocol. There is no other.
const CoalesceDefault = 300 * time.Millisecond
CoalesceDefault is how long a burst of changes gathers before a window is read again.
A feed that fires several times a second would otherwise spend itself re-running the same query. Long enough to turn a salvo into one read, short enough that nobody notices the wait.
const MaxLimit = 200
MaxLimit is the widest window a client may ask for.
Wider than a screen, narrower than a scan. The real ceiling is the wire, and it is not this: some proxies silently drop frames over ~64 kB, so what fits depends on the size of a row and is the source's business.
const NotAuthorised = 1008
NotAuthorised is RFC 6455 policy violation: the socket opened, the caller may not use it.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Envelope ¶
type Envelope struct {
Event string `json:"event"`
Data json.RawMessage `json:"data"`
}
Envelope is how every frame travels, both ways.
type Options ¶
type Options struct {
// Authorize answers whether this caller may use the socket at all.
//
// The only place the library touches your application's idea of identity.
// Nil accepts every upgrade, which is right behind a gateway that has
// already authenticated and wrong on the open internet.
Authorize func(request *http.Request) bool
// Refusal is what to say before closing a socket that was refused. Said on
// the socket and not only in a close code: a refusal arriving as a bare
// disconnection is indistinguishable from a network fault.
Refusal func(request *http.Request) string
// Origins allowed to open a socket. Empty means same-origin only.
Origins []string
// Logger. Nil uses the default.
Logger *slog.Logger
}
Options is how a server is configured.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds the sources this server publishes, and the reads they share.
Explicit registration rather than discovery: Go has no annotations, and a list of what a server serves is worth reading anyway.
func NewRegistry ¶
NewRegistry builds an empty registry. `coalesce` is how long a burst gathers before a read; zero means CoalesceDefault.
func (*Registry) Watch ¶
Watch answers a channel of windows, and a function to stop watching.
Two callers asking the same question share one read: the second is handed what the window already holds, and no query is run. The read stops and the entry is dropped when the last of them leaves — otherwise the map is a leak the size of every filter ever typed.
type Row ¶
type Row struct {
ID string `json:"id"`
// UpdatedAt is the version of this row. It changes whenever anything the
// row shows changes.
//
// Not necessarily a timestamp: a filter entry whose only content is its
// label uses the label, and a row carrying a value derived from the clock
// has to fold that value in — otherwise the server believes the row
// unchanged and never sends it again. See SPEC.md, "Versions".
UpdatedAt string `json:"updatedAt"`
// Data is what the row actually shows. Marshalled flat beside ID and
// UpdatedAt, so the wire carries one object per row rather than a nested
// one — see MarshalJSON.
Data map[string]any `json:"-"`
}
Row is what every row a source publishes must carry.
func (Row) MarshalJSON ¶
MarshalJSON writes id, updatedAt and the row's own fields as one object.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is one endpoint: every subscription of every client passes through it.
It implements http.Handler, so it is mounted wherever the application wants:
mux.Handle("/my-service/ws", livewire.NewServer(registry, livewire.Options{...}))
type Source ¶
type Source interface {
// ReadQuery is the trust boundary: what arrives is JSON off a socket.
// Clamp it, whitelist it, default it, and hand back something Read can act
// on without checking again.
ReadQuery(raw json.RawMessage) (any, error)
// Key answers what two identical questions share. Two queries with the
// same key share one read.
Key(query any) string
// Wake fires whenever this source may have something new to say. Only the
// fact of a send is read, never its value.
//
// It must not be closed while the source is in use; a source with nothing
// to follow returns a channel that never sends.
Wake() <-chan struct{}
// Read is the window as it stands.
Read(ctx context.Context, query any) (Window, error)
}
Source is one live list.
Read returns the whole window, never a delta. Turning it into a patch is the server's business, per subscription, because only it knows what that client actually received.
type Window ¶
type Window struct {
Rows []Row
// Total is the length the window is a page of. Nil when the source does
// not page.
Total *int
// Pivot is an index in the whole list the source points the client at.
//
// A number and nothing more — neither side interprets it. A departure
// board uses it for the boundary between what has left and what has not: a
// position in the list that a client holding one page of six hundred
// cannot work out from the rows it happens to have.
Pivot *int
}
Window is what a source answers with: a window of rows, and what it is a window of.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
conformance
command
Command conformance stands up a Livewire server exposing exactly what the shared scenarios expect, so the TypeScript conformance suite can drive this implementation over a real socket.
|
Command conformance stands up a Livewire server exposing exactly what the shared scenarios expect, so the TypeScript conformance suite can drive this implementation over a real socket. |