protocol

package
v0.2.3 Latest Latest
Warning

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

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

Documentation

Overview

Package protocol is the boundary every byte crosses in either direction.

Every WebSocket message payload, both ways, is exactly one encoded gotthlive.v1.Frame sent as a binary frame. There is no JSON, no text framing, and no debug escape hatch. The schema, its refinement predicates, and the invariants the predicate grammar cannot express are specified in docs/protocol.md.

Inbound

ParseInbound is the sole entry point for bytes arriving from a client, and there is no exported way to obtain a payload that has not passed through it. It unmarshals, applies the generated Liquid Proto validator to the envelope and then to the matched payload, checks version compatibility, walks every enum field against its descriptor, bounds every repeated field, and applies the cross-field invariants. A new frame kind cannot skip a step: a conformance test walks the payload oneof by protoreflect and asserts every member has both a case here and a generated Validate function.

Outbound

ValidateOutbound re-checks a constructed frame against the same boundary immediately before marshalling, on the single write path, and it is not optional. Inbound frames are protected because they are parsed; outbound frames are constructed, and construction discipline is not a property a reviewer can check. Re-checking is what makes "no patch without an origin" a type-level fact rather than a convention.

Layout

Generated code lives in gotthlivepb so that the reproducibility check has a clean target and hand-written code is never mixed with generated code. It holds the frame schema and generated validators. The canonical annotation schema and runtime come from pkg/liquidproto. Generated protocol code is committed, because consumers of this module must never need protoc.

Status

Implemented: ParseInbound, ValidateOutbound, the framer, the limits table, the close-code enumeration, and the hand-checked invariants.

Index

Constants

View Source
const (
	// SourceMount is the origin of the first snapshot of a session.
	SourceMount = "mount"
	// SourceResync is the origin of a snapshot answering a resync request.
	SourceResync = "resync"
	// SourceEventPrefix namespaces a client-caused transition by event name.
	SourceEventPrefix = "event:"
	// SourceEffectPrefix namespaces a transition caused by an effect result.
	SourceEffectPrefix = "effect:"
	// SourceSlowClient is the origin of a transition the library synthesized
	// because the outbound window filled.
	SourceSlowClient = "timer:slow_client"
	// SourceClientRecovered is its counterpart when the window drained.
	SourceClientRecovered = "timer:client_recovered"
	// SourceEffectInvalid stands in for an effect whose own EffectSource()
	// cannot be namespaced into a legal Origin.source. The effect is refused
	// before it runs and the application is told through the ordinary failure
	// event; this is what that event's origin says, because a frame carrying
	// the offending string is exactly what must not be constructed.
	SourceEffectInvalid = "effect:invalid_source"
)

Origin sources. The vocabulary is a convention rather than a schema: a source is whatever an effect reports, so cardinality is bounded at the metric rather than here.

View Source
const (
	ReasonRefineFailed    = "refine_failed"
	ReasonOversize        = "oversize"
	ReasonUnknownKind     = "unknown_kind"
	ReasonBadVersion      = "bad_version"
	ReasonSessionMismatch = "session_mismatch"
	ReasonTextFrame       = "text_frame"
	ReasonEnumDomain      = "enum_domain"
	ReasonListBound       = "list_bound"
	ReasonAckChannelFull  = "ack_channel_full"
)

Rejection reasons. These are the label values of gotthlive_frames_rejected_total{reason}; the set is closed, and one enforcement site owns each value.

View Source
const CoalesceFlushCeiling = 1024

CoalesceFlushCeiling is the H-4 bound on Origin.contributing_event_ids. The actor's own flush threshold defaults to half of it and is validated against it; this is the schema-level assertion that the flush worked.

View Source
const MaxOriginSource = 64

MaxOriginSource is the schema's byte bound on Origin.source (proto/gotthlive/v1/frame.proto, message Origin).

View Source
const Subprotocol = "gotth-live.v1"

Subprotocol is the WebSocket subprotocol token offered on the upgrade. It is a fast reject, not the source of truth: the negotiated version is re-asserted in band as Frame.protocol_version and validated there.

View Source
const Version uint32 = 1

Version is the protocol version this build speaks. A peer whose major version differs is refused with a reason rather than reinterpreted (H-2).

Variables

View Source
var (
	// HeartbeatIntervalMSRange refines Snapshot.heartbeat_interval_ms.
	HeartbeatIntervalMSRange = SessionParamRange{
		Field:     "heartbeat_interval_ms",
		Predicate: "this >= 1000 && this <= 300000",
		Min:       1000,
		Max:       300000,
	}
	// MaxInboundFrameBytesRange refines Snapshot.max_inbound_frame_bytes.
	MaxInboundFrameBytesRange = SessionParamRange{
		Field:     "max_inbound_frame_bytes",
		Predicate: "this >= 1024 && this <= 1048576",
		Min:       1024,
		Max:       1048576,
	}
	// AckWindowRange refines Snapshot.ack_window.
	AckWindowRange = SessionParamRange{
		Field:     "ack_window",
		Predicate: "this >= 1 && this <= 256",
		Min:       1,
		Max:       256,
	}
)

The three session parameters a Snapshot carries, which is what makes them the fields an operator can set and a session can then fail to encode: they are the only configuration that leaves this process as refined wire values (D-23).

Functions

func CheckSessionID

func CheckSessionID(in IInbound, want [16]byte) error

CheckSessionID is H-3: a frame's session_id must equal the session bound to the connection it arrived on.

It is a separate call rather than a ParseInbound step because the expected value is transport state, and the transport ingress is the only place that holds it. A mismatch is a protocol violation and closes the connection: a client that names another session is not confused, it is probing.

func FrameDescriptor

func FrameDescriptor() protoreflect.MessageDescriptor

FrameDescriptor returns the descriptor of the one message on the wire. The conformance tests walk it rather than a hand-maintained list of kinds.

func ListBound

func ListBound(name protoreflect.FullName) (int, bool)

ListBound reports the H-4 bound declared for a repeated field, and whether one is declared at all. It is exported for the descriptor-walk test that holds the table complete.

func NewAck

func NewAck(session [16]byte, serverSeq uint64) *pb.Frame

NewAck builds the one acknowledgement the server sends: the answer to a resync request that describes no gap, where a full snapshot would be waste.

func NewError

func NewError(session [16]byte, code pb.ErrorCode, message string, eventID, clientRef uint64, fatal bool) *pb.Frame

NewError builds an error frame. eventID and clientRef are both zero unless the error concerns one event, which H-12 holds to.

func NewHeartbeat

func NewHeartbeat(session [16]byte, nonce uint64, intervalMS uint32) *pb.Frame

NewHeartbeat builds a heartbeat frame.

func NewPatch

func NewPatch(session [16]byte, c Causal, o Origin, updates []Update) *pb.Frame

NewPatch builds a patch frame. It does not validate: the framer does that on the single write path, so there is exactly one place a constructed frame is checked and no way to reach the socket around it.

func NewSnapshot

func NewSnapshot(session [16]byte, c Causal, o Origin, p SessionParams, s Supersession, updates []Update) *pb.Frame

NewSnapshot builds a snapshot frame carrying every registered fragment and the session parameters.

func ValidOriginSource

func ValidOriginSource(s string) bool

ValidOriginSource reports whether s satisfies Origin.source's predicate: non-empty, at most MaxOriginSource bytes, and matching ^[a-z][a-z0-9_.:/-]*$.

It exists so that a string can be refused by the boundary that owns it rather than by the frame it eventually lands in. Origin.source is composed from application-supplied halves — an event name, an effect's reported source — and a composed value that fails the predicate is dropped by ValidateOutbound on the actor goroutine, three layers from the caller, as an INTERNAL error the application never hears about. That is the D-18 shape, and this is what lets the callers close it: live.New refuses an event name that cannot be namespaced, and the actor turns an unnameable effect into that effect's own deterministic failure.

It is a hand-written second implementation of a compiled predicate, which is a cost. The alternative — constructing a throwaway Origin and calling ValidateOrigin — allocates a message per check on the emit path and reports "some field of some Origin is wrong" rather than naming this one. The conformance suite asserts the two agree over the boundary cases, which is what keeps the duplication honest.

func ValidateOutbound

func ValidateOutbound(f *pb.Frame) error

ValidateOutbound re-checks a constructed frame against every predicate in the schema, plus the cross-field invariants, through the generated Liquid Proto validation boundary. The framer calls it immediately before marshalling, on the single write path, and it is not optional.

Inbound frames are protected because they are parsed. Outbound frames are constructed, and Go cannot forbid the zero value of an opaque type, so without this step a struct literal assembled anywhere in the emit path would produce an orphan patch that nothing catches — the client codec does not enforce the pattern predicates, so neither does an independent decode of the capture. Re-checking is what turns "no patch without an origin" from a discipline into a property.

Types

type Causal

type Causal struct {
	// ServerSeq is the frame's place in the session's outbound order, from 1.
	// It is what an Ack acknowledges and what a client detects a gap in.
	ServerSeq uint64

	// PatchID names this emitted frame, one per Patch or Snapshot, and is what
	// a client's apply-latency report has to name to be believed.
	PatchID uint64

	// TransitionID names the reducer invocation this frame came from, one per
	// invocation including one that changed nothing.
	TransitionID uint64

	// StateVersion rises if and only if the transition changed state, which is
	// how a re-render is told apart from a state change downstream.
	StateVersion uint64
}

Causal is the server-minted chain a sequenced frame carries. Every field is monotonic per session and every field is positive: the predicates make a frame with a hole in its chain unconstructable.

type CloseCode

type CloseCode int

CloseCode is a WebSocket close code from the library's private-range enumeration. Every close names one of these: a connection closed for an unenumerated reason is a defect, not a diagnostic gap, and an architecture test walks every close call site to hold that true.

const (
	// CloseNone is the zero value and is not a close code. It means "this
	// error does not close the connection".
	CloseNone CloseCode = 0

	CloseNormal             CloseCode = 4000 // client or server closed cleanly
	CloseGoingAway          CloseCode = 4001 // server shutting down or draining
	CloseProtocolViolation  CloseCode = 4002 // text frame, non-Frame bytes, H-3, H-7
	CloseUnsupportedVersion CloseCode = 4003 // H-2
	CloseUnauthenticated    CloseCode = 4004 // identity hook failed post-upgrade
	CloseForbiddenOrigin    CloseCode = 4005 // origin allowlist
	CloseUnauthorized       CloseCode = 4006 // authorization hook returned a fatal denial
	CloseFrameTooLarge      CloseCode = 4007 // H-5
	CloseRateLimited        CloseCode = 4008 // inbound limits, including the H-14 resync bucket
	CloseSlowClient         CloseCode = 4009 // outbound window exhausted
	CloseHeartbeatTimeout   CloseCode = 4010 // peer-dead detection
	CloseSessionEvicted     CloseCode = 4011 // idle timeout
	CloseInternalError      CloseCode = 4012 // contained panic that could not be recovered into the session
	CloseResyncFailed       CloseCode = 4013 // resync could not produce a consistent snapshot
)

The enumeration. The numeric value is the wire code; Label is the metric label value, and dashboards, the audit, and this table therefore cannot drift apart.

func CloseCodes

func CloseCodes() []CloseCode

CloseCodes returns the enumeration in ascending order. It exists so tests and the wire audit iterate the real table rather than a copy of it.

func (CloseCode) Label

func (c CloseCode) Label() string

Label returns the lower-case metric label value for c. An unenumerated code returns "unenumerated", which is a value no correct run can produce and is therefore an alarm rather than a silent hole.

func (CloseCode) Valid

func (c CloseCode) Valid() bool

Valid reports whether c is a member of the enumeration.

type Encoded

type Encoded struct {
	// contains filtered or unexported fields
}

Encoded is one frame that has passed ValidateOutbound and been marshalled: the only thing Write accepts, and the only thing this package will put on a socket.

Its fields are unexported and it has no constructor, so Framer.Encode is the only way to obtain a non-zero one and Write's argument cannot be assembled by hand. That is U-5's point. B-9 — every socket write in the module goes through Encode, which calls ValidateOutbound and refuses on failure — was true by grep and stayed true by nobody wanting to break it; Write took pre-encoded bytes and a Kind, so a future caller could marshal a frame itself and hand them over, and the coupling between "validated" and "written" was a convention rather than an invariant. A token only Encode can mint makes the bypass unconstructable instead of merely unobserved.

The split it preserves is not decorative: instrumentation §2.3 defines gotthlive_send_duration_seconds as "time in Conn.Write, the write-stall signal", and while validate, marshal and write were one call that histogram and gotthlive_encode_duration_seconds were equal by construction and neither could isolate a stalling client. Collapsing Write back into Send to unexport it would re-create exactly that (L9 ruling on U-5).

func (Encoded) Kind

func (e Encoded) Kind() Kind

Kind reports which payload the encoded frame carries.

func (Encoded) Len

func (e Encoded) Len() int

Len is the encoded size in bytes, which is what the frame-size attributes and the byte counters record.

type EventField

type EventField struct {
	// Key is the validated form-control name. It is non-empty, at most 128
	// bytes, and contains only ASCII letters, digits, underscores, periods,
	// brackets, or hyphens.
	Key string
	// Value is the copied form-control value. Validation limits it to 8 KiB.
	Value string
}

EventField is one validated event form field copied out of the decoded protobuf. Its string values do not alias mutable wire storage.

type Framer

type Framer struct {

	// OnSent is called after a frame reaches the transport, with the encoded
	// size. It is the only incrementer of the frames-sent counter.
	OnSent func(kind Kind, bytes int)
	// OnInvalid is called when a constructed frame fails ValidateOutbound.
	// The frame was built on this side, so it is never a client problem, and
	// any occurrence is actionable. It is not necessarily a coding bug: some
	// of what a frame carries comes from the application, and until D-18 an
	// unbounded Event.Contributing arrived here as one.
	OnInvalid func(kind Kind, err error)
	// contains filtered or unexported fields
}

Framer is the single write path. Nothing reaches the socket except through it, which is what makes the emitted-frame counter checkable against a wire capture: any drift means a second write path exists.

It serializes writes with a mutex. That is not the actor model leaking: the socket is shared transport infrastructure rather than session state, and it has two legitimate writers. The actor writes every patch, snapshot and heartbeat; the read pump writes the error frame for an inbound frame it refuses before that frame ever reaches the mailbox — which it must be able to do precisely when the mailbox is full, since that is what the rejection is about.

func NewFramer

func NewFramer(w WriteFunc) *Framer

NewFramer returns a framer writing through w.

func (*Framer) Encode

func (f *Framer) Encode(frame *pb.Frame) (Encoded, error)

Encode validates and marshals one frame without touching the socket.

It is separated from Write because the two are different work with different failure modes, and until FR-36's gotthlive.send span was implemented nothing in this library could tell them apart. The consequence was not only a missing span: instrumentation §2.3 defines gotthlive_send_duration_seconds as "time in Conn.Write, the write-stall signal", and with one combined call site the actor recorded the same interval — validate plus marshal plus write — into both that histogram and gotthlive_encode_duration_seconds. Two series that are equal by construction cannot detect the stall one of them is named for.

func (*Framer) Send

func (f *Framer) Send(ctx context.Context, frame *pb.Frame) (int, error)

Send validates, encodes and writes one frame, returning the number of bytes written. A frame that fails validation is never written and is reported as *InvalidFrameError.

func (*Framer) Write

func (f *Framer) Write(ctx context.Context, e Encoded) (int, error)

Write puts one encoded frame on the wire under the framer's mutex.

It stays a method on the framer rather than becoming a second write path: the serialization and the sent-frame counter are here, which is what makes the counter checkable against a wire capture at all (protocol.md P8).

It takes an Encoded rather than bytes and a Kind, so the only thing reachable here is something Encode validated. The zero value is the one Encoded a caller outside this package can name, and it is refused: an empty payload is not a frame, and accepting it would leave exactly the hole the type closes.

type IInbound

type IInbound interface {

	// Kind reports which payload this variant carries.
	Kind() Kind
	// Envelope returns a fresh copy of the validated frame envelope. Mutating
	// the copy cannot alter the accepted IInbound value.
	Envelope() *pb.Frame
	// contains filtered or unexported methods
}

IInbound is a closed sum type over the frames a client may send. Values returned by ParseInbound hold immutable scalar snapshots copied only after the generated Liquid Proto validators succeed.

The type is closed by an unexported method: a package outside this one cannot add a variant, which is what makes the switch in the session ingress exhaustive in fact and not merely by convention.

func ParseInbound

func ParseInbound(b []byte, limits Limits) (IInbound, error)

ParseInbound is the sole entry point for bytes arriving from a client. There is no exported way to obtain an inbound payload that has not passed through it.

It runs, in order: unmarshal into the generated frame, which is also where protobuf's own UTF-8 validation of string fields happens; the envelope refinement boundary; the version compatibility check; the payload's own refinement, per element for repeated messages; the enum domain walk; and the list cardinality walk. A new payload kind cannot skip a step, because the switch below is the only way to reach a payload and a conformance test walks the oneof descriptor to assert every member has a case here.

Errors are always *RejectError, so the caller has the metric label, the reply code and the close code without re-deriving any of them.

type InboundAck

type InboundAck struct {
	// contains filtered or unexported fields
}

InboundAck reports the highest contiguous server_seq the client has applied.

func (InboundAck) Envelope

func (b InboundAck) Envelope() *pb.Frame

func (InboundAck) Kind

func (b InboundAck) Kind() Kind

func (InboundAck) ServerSeq

func (a InboundAck) ServerSeq() uint64

ServerSeq is the validated cumulative high-water mark.

type InboundClientTelemetry

type InboundClientTelemetry struct {
	// contains filtered or unexported fields
}

InboundClientTelemetry reports how long the browser took to apply a patch. Every field of it is untrusted input.

func (InboundClientTelemetry) ApplyMicros

func (t InboundClientTelemetry) ApplyMicros() uint32

ApplyMicros is the validated total client-side patch application duration.

func (InboundClientTelemetry) Envelope

func (b InboundClientTelemetry) Envelope() *pb.Frame

func (InboundClientTelemetry) Kind

func (b InboundClientTelemetry) Kind() Kind

func (InboundClientTelemetry) MorphMicros

func (t InboundClientTelemetry) MorphMicros() uint32

MorphMicros is the validated client-side DOM morph duration.

func (InboundClientTelemetry) PatchID

func (t InboundClientTelemetry) PatchID() uint64

PatchID is the validated patch identifier the telemetry describes.

type InboundEvent

type InboundEvent struct {
	// contains filtered or unexported fields
}

InboundEvent is one interaction raised by the browser. Fields are refined element by element, because a repeated message field is neither refined nor bounded by the generated boundary.

func (InboundEvent) ClientRef

func (e InboundEvent) ClientRef() uint64

ClientRef is the validated client-local correlation identifier.

func (InboundEvent) Envelope

func (b InboundEvent) Envelope() *pb.Frame

func (InboundEvent) Fields

func (e InboundEvent) Fields() []EventField

Fields returns a copy of the event's validated form fields.

func (InboundEvent) FragmentID

func (e InboundEvent) FragmentID() string

FragmentID is the validated fragment the event targets.

func (InboundEvent) Kind

func (b InboundEvent) Kind() Kind

func (InboundEvent) Name

func (e InboundEvent) Name() string

Name is the validated registered event name.

func (InboundEvent) SeenServerSeq

func (e InboundEvent) SeenServerSeq() uint64

SeenServerSeq is the latest server sequence the client had observed.

type InboundHeartbeat

type InboundHeartbeat struct {
	// contains filtered or unexported fields
}

InboundHeartbeat is the liveness signal.

func (InboundHeartbeat) Envelope

func (b InboundHeartbeat) Envelope() *pb.Frame

func (InboundHeartbeat) Kind

func (b InboundHeartbeat) Kind() Kind

type InboundResyncRequest

type InboundResyncRequest struct {
	// contains filtered or unexported fields
}

InboundResyncRequest asks for a full re-render. It is the one client frame that reaches the actor and triggers work proportional to the whole state, so it is rate limited in a bucket of its own.

func (InboundResyncRequest) Envelope

func (b InboundResyncRequest) Envelope() *pb.Frame

func (InboundResyncRequest) Kind

func (b InboundResyncRequest) Kind() Kind

func (InboundResyncRequest) LastAppliedSeq

func (r InboundResyncRequest) LastAppliedSeq() uint64

LastAppliedSeq is the validated last contiguous sequence the client applied.

func (InboundResyncRequest) Reason

Reason is the validated reason the client requested a full snapshot.

type InvalidFrameError

type InvalidFrameError struct {
	// Kind is the payload the refused frame carried, which is what a log line
	// needs to say which emit path built it.
	Kind Kind

	// Err is the validation failure, typically a refinement violation naming
	// the offending field.
	Err error
}

InvalidFrameError reports a frame this library constructed and could not validate. It is separated from a transport failure because the two demand different responses: a transport failure ends the connection, while an invalid frame is dropped and replaced by an Error carrying the same causal chain, leaving the sequence contiguous.

func (*InvalidFrameError) Error

func (e *InvalidFrameError) Error() string

Error says explicitly that the server built this frame, because the first question asked of a validation failure is whose input caused it and here the answer is never the client's.

func (*InvalidFrameError) Unwrap

func (e *InvalidFrameError) Unwrap() error

Unwrap exposes the validation failure to errors.Is and errors.As, so a caller can reach the *liquidproto.Error and name the field.

type Kind

type Kind int

Kind names one member of the Frame payload oneof. It is the domain of the kind label on gotthlive_frames_received_total and _sent_total.

const (
	KindUnknown Kind = iota
	KindEvent
	KindAck
	KindHeartbeat
	KindClientTelemetry
	KindResyncRequest
	KindPatch
	KindSnapshot
	KindError
)

The eight kinds. KindUnknown is not a member of the oneof: it is what a frame carrying no payload, or a payload from a newer schema, parses as, and it is always a rejection rather than a silent pass.

func KindByField

func KindByField(name protoreflect.Name) (Kind, bool)

KindByField returns the Kind of a payload oneof member by its proto field name, and whether one is declared. Exported for the conformance test.

func KindOf

func KindOf(f *pb.Frame) Kind

KindOf reports which payload a frame carries.

func (Kind) ClientToServer

func (k Kind) ClientToServer() bool

ClientToServer reports whether a client may send this kind. A server-only kind arriving from a client is a protocol violation, not a curiosity.

func (Kind) String

func (k Kind) String() string

String returns the metric label value for k.

type Limits

type Limits struct {
	// MaxInboundFrameBytes caps a decoded frame (H-5).
	MaxInboundFrameBytes int
}

Limits are the inbound bounds this package enforces. They are the subset of the library's limits that the parse boundary needs; the rest belong to the session actor and the transport.

MaxInboundFrameBytes is the authoritative one. It is applied to the connection before any payload is allocated, so it bounds memory rather than merely rejecting after the fact; the per-field and per-list bounds are subordinate defence in depth that fail early with a field-specific error.

func DefaultLimits

func DefaultLimits() Limits

DefaultLimits returns the documented defaults.

type Origin

type Origin struct {
	// Kind is the category of cause. The zero value is never emitted: the
	// outbound boundary refuses a frame that names no category.
	Kind pb.OriginKind

	// EventID is the server-minted identity of the causing event, zero when
	// the server started the transition itself. It is unforgeable, which is
	// what makes it the authoritative root of the chain.
	EventID uint64

	// ClientRef is the client's own correlation handle, echoed unchanged. It
	// is the only value here that untrusted input chose, and it is carried
	// rather than trusted: nothing downstream keys on it.
	ClientRef uint64

	// Source is the specific cause, such as "event:cart.add". It is composed
	// from application-supplied halves, so ValidOriginSource is what keeps a
	// composed value from failing three layers away as an INTERNAL error.
	Source string
	// Contributing lists events whose state changes this patch carries but
	// which were not individually patched, because coalescing collapsed them.
	Contributing []uint64
}

Origin says what caused a patch. Nothing is ever emitted without one.

type RejectError

type RejectError struct {
	// Reason is the gotthlive_frames_rejected_total label value.
	Reason string
	// Code is the ErrorCode carried back to the client.
	Code pb.ErrorCode
	// Close names the close code when this rejection ends the connection, and
	// is CloseNone when the connection survives.
	Close CloseCode
	// Detail is the operator-facing explanation. It never carries a token, a
	// cookie, an authorization input, application state, or a raw frame body.
	Detail string
	// Err is the underlying cause, typically a refinement violation.
	Err error
}

RejectError reports a frame this build refuses to accept, and carries everything the caller needs to answer it without re-deriving anything: the metric label, the ErrorCode for the reply frame, and the close code when the violation is fatal to the connection.

The message follows the library's error template — what failed, why, and what to do — because a rejection an operator cannot act on is a defect (FR-58).

func (*RejectError) Error

func (e *RejectError) Error() string

Error renders the reason label, the operator-facing detail and the underlying cause in that order: the label is what a metric was incremented with, so a log line and a counter can be lined up without a lookup table.

func (*RejectError) Fatal

func (e *RejectError) Fatal() bool

Fatal reports whether this rejection closes the connection.

func (*RejectError) Unwrap

func (e *RejectError) Unwrap() error

Unwrap exposes the underlying refinement violation to errors.Is and errors.As, so a caller can inspect *refine.Error for the offending field.

type SessionParamRange

type SessionParamRange struct {
	// Field is the Snapshot field the predicate is declared on.
	Field string
	// Predicate is the predicate's source text, verbatim from the .proto. It
	// is carried so that a rejection can say where the range comes from
	// instead of asserting one on its own authority.
	Predicate string
	// Min and Max are the interval's inclusive endpoints.
	Min, Max uint32
}

SessionParamRange is the closed interval one Snapshot session parameter's refinement admits.

The predicates are declared on gotthlive.v1.Snapshot in proto/gotthlive/v1/frame.proto (protocol.md §3.3) and compiled to native Go by protoc-gen-liquidproto, which emits a message validator and no constant for either endpoint. So a caller that wants to reject an out-of-range value at construction, rather than discover it when the frame it built is refused on the write path, has a validator to ask and no number to quote back to the operator. These are that number, in the one place this side of the wire names it.

They are not a second opinion about the range. The generated validator stays the only thing that decides a frame, and sessionparams_test.go holds every range below against the production outbound boundary that invokes it: accepted at Min and at Max, refused one past either end, with Field and Predicate compared verbatim against what the generator compiled. A predicate that moves in the .proto and is regenerated turns those specs red rather than leaving this file quietly wrong.

func (SessionParamRange) Contains

func (r SessionParamRange) Contains(v int64) bool

Contains reports whether v is within the range.

It takes an int64 rather than a uint32 because every caller is narrowing from something wider — a time.Duration counted in milliseconds, an int — and narrowing first is how a value far outside the range becomes one inside it: 4294987296 ms is 49 days, and is 20 seconds once truncated to uint32.

type SessionParams

type SessionParams struct {
	// HeartbeatIntervalMS is how often the client should send a heartbeat.
	// The schema refines it to 1000..300000, so a value outside that builds a
	// frame this library then refuses to send.
	HeartbeatIntervalMS uint32

	// MaxInboundFrameBytes is the largest frame the server will read from this
	// client. Refined to 1024..1048576, for the same reason.
	MaxInboundFrameBytes uint32

	// AckWindow is how many unacknowledged patches the server will hold before
	// it stops emitting. Refined to 1..256.
	AckWindow uint32
}

SessionParams are the values a session sends once, in its first snapshot, so that the client needs no configuration of its own.

type Supersession

type Supersession struct {
	// FromSeq is the first replaced server_seq, inclusive.
	FromSeq uint64

	// ThroughSeq is the last replaced server_seq, inclusive. A range rather
	// than the union of the contributing event identifiers, because the union
	// is unbounded and this is two varints.
	ThroughSeq uint64
}

Supersession is the inclusive server_seq range a resync snapshot replaces. Both fields are zero on a session's first snapshot.

type Update

type Update struct {
	// FragmentID is the region this markup belongs to.
	FragmentID string

	// Op is how the client applies it. The zero value is refused at the
	// outbound boundary rather than arriving as a silent morph.
	Op pb.PatchOp

	// HTML is the region's complete markup, not a diff: the diff happens in
	// the browser, against the live DOM.
	HTML string
}

Update is the new markup for one live region.

type WriteFunc

type WriteFunc func(ctx context.Context, b []byte) error

WriteFunc puts one already-encoded frame on the wire. It is a function value rather than an interface because there is one implementation and a one-implementation interface buys nothing; it is what keeps the core packages from naming the transport at all.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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