livetest

package
v0.2.6 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: 24 Imported by: 0

Documentation

Overview

Package livetest provides testing helpers for live applications.

It exists as a package separate from live so that production binaries do not link testing, and with it flag, regexp, runtime/pprof and runtime/trace, merely because they import a UI library. The precedent is net/http/httptest and testing/fstest; the separation is asserted by an architecture test rather than left as an intention.

Four kinds of helper live here.

Every helper below takes a testing.TB, and nothing here adapts one. A plain go test suite passes its *testing.T. A Ginkgo suite passes ginkgo.GinkgoTB(), which Ginkgo ships for exactly this — "a wrapper that exactly matches the testing.TB interface … intended to be used as a drop-in replacement with third party libraries that accept testing.TB". This package therefore imports no spec framework and needs no adapter to avoid importing one. It shipped such an adapter briefly, on the false premise that a Ginkgo suite could not produce a testing.TB — that is true of GinkgoT() and false of GinkgoTB(); see docs/reviews/rulings-review-wave.md, ruling 1.

Construction. NewSession builds the github.com/candacelabs/csf/pkg/gotth/live.Session a Config hook is called with, so Init, Authorize, Teardown and Execute can be driven from a spec without a running server. A Session's fields are unexported because identity is bound at the handshake and nothing downstream may mint one; that is right for production and leaves a test unable to build the one value every hook takes.

Determinism. ReplayN replays an event log against a reducer several times and fails unless the resulting state and the emitted effects are identical on every run. A reducer that reads a clock, a random source, or the iteration order of a map fails it. AssertDirtyComplete catches the dual mistake in rendering: a fragment that declared itself unchanged while its rendered bytes moved.

End-to-end. NewClient dials an http.Handler and returns a Client driving a real session over the real wire protocol, so a spec exercises the same handshake, framing and acknowledgement path a browser does. It decodes with the library's own generated types and hands back plain values — Frame, Patch, Origin, Update, Error — so a spec can assert on the wire without the schema's generated Go reaching its import graph. It never acknowledges a patch on its own: "this client stopped acknowledging" is the condition every backpressure spec is built on, and Ack is explicit for that reason.

Audit. Audit runs a scripted workload and cross-checks every signal the library reports about itself against an independent, out-of-process measurement, on the principle that a metric only the incrementing code can vouch for is not evidence.

Status

NewSession, ReplayN, AssertDirtyComplete and Client are implemented.

Audit, which cross-checks the library's self-reported signals against an out-of-process measurement, is not. Its named consumer is no longer the benchmark harness — that landed as Node and CDP and will never call a Go client — and no consumer has written its shape yet, so exporting a guess at it would be the speculation FR-65 refuses. Client's shape, by contrast, was written twice by hand before it was built here; see docs/api-surface.md §6.

Example

Example is the decoded frame a spec asserts on, and the three questions it is built to answer.

The harness that produces one — NewClient, dialing a real socket against a real handler — takes a testing.TB, and a godoc example takes no arguments, so it cannot appear here. What it hands back can: a Frame is plain values with no generated protobuf type anywhere in the assertion, which is the property api-surface.md §6 records and the reason this view exists at all. The value below is written out by hand for that reason; in a spec it comes from Client.Next, Client.Await or Client.WaitFor.

The three questions, in the order they are usually asked:

  • Did this patch touch a region? Fragment answers with two values, so "the region rendered empty" and "the region was not in this patch" are different answers. The second is the independent-live-regions property.
  • Which regions did it carry? FragmentIDs, in wire order.
  • What did it cost? HTMLBytes, with the per-region split, because "the snapshot was N bytes" is only useful beside which region spent them.

Frame's String is what a failure message prints. It leads with the causal chain rather than the markup, because the markup is rarely why a spec failed.

package main

import (
	"fmt"

	"github.com/candacelabs/csf/pkg/gotth/live/livetest"
)

func main() {
	frame := &livetest.Frame{
		Kind: livetest.FramePatch,
		Patch: &livetest.Patch{
			ServerSeq:    7,
			PatchID:      7,
			TransitionID: 9,
			StateVersion: 4,
			Origin: livetest.Origin{
				Kind:      1, // CLIENT_EVENT
				EventID:   12,
				ClientRef: 3,
				Source:    "event:counter.increment",
			},
			Updates: []livetest.Update{
				{FragmentID: "counter", HTML: `<b data-gotth-region="counter">42</b>`},
				{FragmentID: "total", HTML: `<span data-gotth-region="total">42</span>`},
			},
		},
	}

	html, carried := frame.Patch.Fragment("counter")
	fmt.Printf("counter carried=%t html=%s\n", carried, html)

	_, carried = frame.Patch.Fragment("controls")
	fmt.Printf("controls carried=%t\n", carried)

	fmt.Println("regions:", frame.Patch.FragmentIDs())

	total, per := frame.Patch.HTMLBytes()
	fmt.Printf("html bytes: total=%d counter=%d total-region=%d\n", total, per["counter"], per["total"])

	fmt.Println(frame)

}
Output:
counter carried=true html=<b data-gotth-region="counter">42</b>
controls carried=false
regions: [counter total]
html bytes: total=78 counter=37 total-region=41
patch{seq=7 origin=event:counter.increment/1 fragments=[counter total] contributing=[]}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func AssertDirtyComplete

func AssertDirtyComplete[S any, I live.IIdentity](tb testing.TB, cfg live.Config[S, I], initial S, log []live.Event)

AssertDirtyComplete replays a log against a configuration and fails if any fragment declared itself unchanged while its rendered bytes moved.

Under-declaring is the one rendering mistake that produces a stale region in production and nothing at all in development, because the fragment is usually re-rendered by some other transition before anybody looks. This is what catches it before merge.

Over-declaring is not a failure: a render whose bytes did not change is suppressed, so declaring too much costs a comparison and nothing else.

func JSString added in v0.2.0

func JSString(value string) string

JSString renders a Go string as a JavaScript string literal, through the JSON encoder, so a selector containing double quotes stays one literal.

func NewSession

func NewSession[I live.IIdentity](tb testing.TB, id live.ID, identity I) live.Session[I]

NewSession returns the live.Session a spec needs in order to call an application's own Config hooks directly.

live.Config's Init, Authorize, Teardown and Execute all take a Session, and a Session's fields are unexported because identity is bound at the handshake and nothing downstream may mint one. The consequence for a test is sharper than it first looks: live.Session{} compiles anywhere — an empty composite literal names no field — but its ID() is all-zero and its Identity() is nil, and identity is the reason those hooks take a Session at all. So an application can construct a useless Session and cannot construct a useful one, and a hook that takes one is testable only through a running server. Every application that unit-tests a hook otherwise invents the same workaround: a second, unexported method taking the values the hook would have read, with the exported hook reduced to an adapter over it. This library's own counter example carried exactly that split, and its comment was a defect report.

Both values are the caller's, deliberately. Deriving the identifier from the identity would be tempting and wrong: one subject holds many concurrent sessions — that is what Limits.MaxSessionsPerIdentity is about — so two tabs belonging to one user need two identifiers and one identity.

The nil-identity guard this used to carry is gone, and the type parameter is why. Since 2026-09-03 the identity is the application's OWN type rather than an interface, so "identity is nil" is not a value a caller can pass unless it chose a pointer type and passed a nil of it — which is its own bug, in its own Subject(), rather than a trap this constructor sets.

It takes a testing.TB first, matching ReplayN and AssertDirtyComplete, and that is a guard and not decoration: reaching this constructor from production code means importing a package that links testing and then fabricating a testing.TB, which is a visible and absurd act rather than an accident. The second guard is the token, which only live and live/livetest can obtain.

func ReplayN

func ReplayN[S any, I live.IIdentity](tb testing.TB, reduce live.Reducer[S, I], initial S, log []live.Event, n int)

ReplayN replays an event log against a reducer n times and fails unless the resulting state and the emitted effects are identical on every run.

It is the determinism harness the library requires rather than suggests. A reducer that reads a clock, a random source, or the iteration order of a map fails it, and those three are the whole of what usually goes wrong: nothing else in a pure function of two values can differ between runs.

State is compared deeply. Effects are compared by the SEQUENCE OF SOURCES they were declared under, because live.Effect carries its behaviour in a function field and Go cannot compare two function values: two closures built by the same line of the same reducer are never equal, so a deep comparison of effects would fail every determinism check rather than passing the honest ones.

That is a narrowing, and it is worth stating plainly. Before the 2026-09-03 ruling made the effect concrete, an effect was a comparable struct and this harness caught a reducer that scheduled `Change{Delta: 1}` on one run and `Change{Delta: 2}` on the next. It now catches a reducer that scheduled a DIFFERENT effect, or a different number of them, or them in a different order — the shape a clock, a random source or a map range actually produces — and no longer catches the same effect carrying a different argument. An effect worth telling apart is therefore worth naming apart.

Types

type Browser added in v0.2.0

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

Browser is one headless Chromium with one attached page, driven over the Chrome DevTools Protocol by the WebSocket library this module already depends on.

It is written rather than imported for the reason FR-74 states: every browser-automation library on offer arrives through npm with a lockfile and a post-install download, and the bench quarantine exists so that none of that reaches a consumer. It implements what a browser spec needs — launch, attach to a page, evaluate JavaScript, read back a JSON value, take a screenshot — and is not a general automation library.

The browser starts through the process capability, so a spec that holds a gomock launcher proves the same crossing a binary would make, and the real one starts the child in its own process group and kills the group on Kill.

func LaunchBrowser added in v0.2.0

func LaunchBrowser(tb testing.TB, launcher proc.ILauncher, options BrowserOptions) *Browser

LaunchBrowser starts headless Chromium through launcher and attaches to a fresh page. Everything it starts is released through tb.Cleanup: the page's connection, then the process, then the profile the caller owns.

func (*Browser) Call added in v0.2.0

func (browser *Browser) Call(method string, params any, out any)

Call sends one protocol command to the attached page and decodes its result into out, which may be nil, failing the spec on any error. It is the escape hatch for what the typed methods do not cover — synthesised input, most often — and params and out are any because the protocol's envelope is.

func (*Browser) EvalBool added in v0.2.0

func (browser *Browser) EvalBool(expression string) bool

EvalBool evaluates an expression producing a boolean.

func (*Browser) EvalJSON added in v0.2.0

func (browser *Browser) EvalJSON(expression string, out any)

EvalJSON evaluates expression in the page, awaiting a promise, and decodes the JSON value it produced into out, failing the spec on any error. out is any for the reason json.Unmarshal's is: the page decides the shape.

func (*Browser) EvalString added in v0.2.0

func (browser *Browser) EvalString(expression string) string

EvalString evaluates an expression producing a string.

func (*Browser) Navigate added in v0.2.0

func (browser *Browser) Navigate(url string)

Navigate loads url and returns once the document has finished loading.

func (*Browser) OnNewDocument added in v0.2.0

func (browser *Browser) OnNewDocument(source string)

OnNewDocument installs a script that runs before any page script, on every document this page loads: how a listener is in place before the runtime it is watching.

func (*Browser) Screenshot added in v0.2.0

func (browser *Browser) Screenshot() []byte

Screenshot captures the page as PNG bytes, for evidence a report keeps.

func (*Browser) TryEvalJSON added in v0.2.0

func (browser *Browser) TryEvalJSON(expression string, out any) error

TryEvalJSON is EvalJSON handing the error back instead of failing the spec: a protocol error, an exception the page threw, or a value that does not decode. A spec polling a page across a reload needs it, because an evaluate that lands between the old execution context and the new one is refused, and that refusal is on that spec's happy path.

func (*Browser) Version added in v0.2.0

func (browser *Browser) Version() string

Version is the browser's product string, for a report entry.

type BrowserOptions added in v0.2.0

type BrowserOptions struct {
	// Executable is the Chromium binary. The bench image names it in
	// CHROME_BIN; a spec reads that variable and skips when it is unset,
	// because the library image deliberately has no browser.
	Executable string

	// Profile is a directory the browser may write its profile into. The
	// caller owns it: a spec passes tb.TempDir(), and the directory is
	// removed after the process has exited rather than while it still writes.
	Profile string

	// Timeout bounds the whole browser session. It defaults to five minutes.
	Timeout time.Duration
}

BrowserOptions configures a launch.

type Client

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

Client drives one session over the real protocol against an http.Handler: a real dial, a real upgrade, real frames in both directions.

It is a browser as far as the server can tell, with one deliberate difference: it never acknowledges a patch on its own. "This client stopped acknowledging" is the condition the backpressure specs are built on, and a helpful auto-ack here would make the whole ladder unreachable — so Ack is explicit, always, including in WaitFor.

Every retrieval method takes an explicit timeout rather than reading the spec's deadline, because "no frame arrived within 5s" and "the suite's 60s budget expired" are different failures and only the first one names what was being waited for.

A Client's frames are decoded with the library's own generated types and handed back as the plain values below, so a spec asserts on the wire without the protocol's generated types reaching its import graph — the property api-surface.md §6 records, held here rather than by not decoding at all.

func NewClient

func NewClient(tb testing.TB, h http.Handler, opts ClientOptions) *Client

NewClient dials h and returns once the mount snapshot has arrived.

It serves h from an httptest.Server rather than taking a URL, so a spec never has a server it forgot to close: the server, the connection and the read goroutine are all released through tb.Cleanup, in that order, whether the spec passed or failed.

Returning only after the snapshot is not convenience. H-10 makes the first frame on a connection the snapshot, and a Client handed back before it arrived would have no session identifier, so its first Send would be a frame the server refuses for a reason that has nothing to do with the spec.

func (*Client) Ack

func (c *Client) Ack(serverSeq uint64)

Ack acknowledges a patch, which is the whole of the window protocol's client half. Withholding it is how a spec makes a client slow without throttling a socket.

func (*Client) Await

func (c *Client) Await(what string, timeout time.Duration, pred func(frame *Frame) bool) *Frame

Await takes frames until one satisfies pred, and fails naming what it did see instead.

The what argument is the failure message's subject — "a meters patch", "the resync snapshot" — and it is required because the alternative message is "the predicate never matched", which tells a reader nothing they can act on.

func (*Client) Close

func (c *Client) Close() error

Close ends the session the way a browser closing a tab does and releases everything this Client owns: the connection, the read goroutine, and the server it dialled.

Calling it is optional — NewClient registered the same release with tb.Cleanup — so it is **idempotent**, returning the first close's result to every later caller. Without that, every spec which closed explicitly would also fail in cleanup, which would make the method unusable for the one thing it exists for.

Releasing the server here rather than only at cleanup is what makes a connect/disconnect loop a real one. A spec that opens twenty tabs to prove the library leaks neither a subscription nor a goroutine would otherwise be measuring twenty servers this package had not got round to closing, and would fail on the harness rather than on the library.

func (*Client) Closed

func (c *Client) Closed(timeout time.Duration) bool

Closed reports whether the server hung up within the timeout. It is how a spec asserts that a fatal error closed the connection rather than merely arriving on it.

func (*Client) Next

func (c *Client) Next(timeout time.Duration) *Frame

Next is NextErr, failing the spec rather than returning the error.

func (*Client) NextErr

func (c *Client) NextErr(timeout time.Duration) (*Frame, error)

NextErr returns the next frame that is not a heartbeat, or an error naming the session it was waiting on and what happened instead.

Heartbeats are skipped because no assertion in any consumer of this package is about a heartbeat's position in the stream, and a suite that has to filter them by hand writes the filter in every await.

The error carries where's prefix — "livetest: <name> (session <hex>): …" — and it is the same string Next fails the spec with. That is deliberate and it is the FR-58 clause: this is the path that hands the value to a caller, so it is the one where this package is not the last reader, and an error a spec stores, wraps or logs beside the server's own records has to say which session it is about. It did not until 2026-08-05: where was applied on the tb.Fatalf paths and nowhere else, so the five docs/error-audit.md §3.4 rows that graded these messages as carrying a session were true of Next and Await and false here. QA-1 caught it by driving it (phase-4-grading.md F-1).

func (*Client) Received

func (c *Client) Received() []*Frame

Received is every frame this client has decoded, heartbeats included, in arrival order. Unlike the retrieval methods it consumes nothing.

func (*Client) Resync

func (c *Client) Resync(lastApplied uint64, reason int32)

Resync asks for a snapshot, claiming to hold everything through lastApplied.

A lastApplied already equal to the server's sequence is answered with an Ack rather than a Snapshot — the no-op short circuit — so a spec measuring what a resync costs passes a value behind the server's on purpose.

func (*Client) Send

func (c *Client) Send(name, fragmentID string, fields map[string]string) uint64

Send writes one event frame and returns the client reference it used.

The reference is the handle a provenance assertion resolves to a server-side event identifier, which is why it is returned rather than kept: correlating a log line to the interaction that caused it is the assertion, and a client that hides the correlator cannot make it.

Fields are sent in sorted key order, so a spec that asserts on the encoded frame is asserting on a stable one.

func (*Client) Seq

func (c *Client) Seq() uint64

Seq is the highest server sequence this client has seen, which is what an event frame reports as its seen_server_seq and what a resync request would claim to hold.

func (*Client) SessionID

func (c *Client) SessionID() []byte

SessionID is the identifier the server bound to this session at the handshake, as it appears in every frame in both directions.

func (*Client) Settle

func (c *Client) Settle(idle time.Duration) []*Frame

Settle drains whatever is in flight and returns once nothing has arrived for the idle period.

func (*Client) Snapshot

func (c *Client) Snapshot() *Frame

Snapshot is the mount snapshot this session opened with.

func (*Client) WaitFor

func (c *Client) WaitFor(fragmentID string, pred func(html string) bool) *Frame

WaitFor blocks until a patch or snapshot makes one fragment's markup satisfy pred, and returns the frame that did it.

It does not acknowledge what it consumed. A spec that wants to behave like a browser calls Ack on the returned frame's sequence; one that is building backpressure does not, and that difference is the point of leaving it out.

func (*Client) WriteRaw

func (c *Client) WriteRaw(payload []byte) error

WriteRaw sends bytes the client did not build, for the specs that are about what the server does with a frame no correct client would send.

It returns the error rather than failing, because "the write was refused" is frequently the assertion.

type ClientOptions

type ClientOptions struct {
	// Path is the request path the live handler is mounted at, as the
	// application's router spells it — "/dashboard/live", not "/".
	Path string

	// Origin is the value of the Origin header the handshake carries. It must
	// be one the application's Config.Origins admits; a rejected origin fails
	// the dial, which is the intended way to spec the refusal.
	Origin string

	// Header carries anything else the handshake needs — a session cookie for
	// an application that authenticates from one, most often. A browser cannot
	// set headers on a WebSocket handshake, so anything here that is not a
	// cookie is testing a path a browser does not have.
	Header http.Header

	// Timeout bounds the whole session, not one read. It defaults to 60s and
	// exists because coder/websocket closes a connection whose read context is
	// cancelled — a per-read deadline would make a slow spec look like a
	// server that hung up.
	Timeout time.Duration
}

ClientOptions configures a dial.

Path and Origin are both required and both are the application's, not the library's: the handler is mounted wherever the application's router put it, and Config.Origins is an allowlist the application wrote. Guessing either one would turn a misconfiguration into a hang.

type Error

type Error struct {
	// Code is the machine-readable classification, from frame.proto's
	// ErrorCode: 1 unsupported version, 2 unauthorized, and so on. It is what
	// a spec should assert on, because Message is deliberately uninformative
	// outside dev.
	Code int32

	// Message is generic in production and detailed when Config.Dev is set.
	// Asserting on its text is asserting on which mode the server was in.
	Message string

	// EventID names the event that caused the failure, zero when the server
	// started the transition itself.
	EventID uint64

	// ClientRef echoes the client's handle for that event, so a browser can
	// mark the right pending interaction as failed.
	ClientRef uint64

	// Fatal says the server is closing the connection after this frame. A
	// non-fatal error leaves the session running with its state unchanged.
	Fatal bool
}

Error is a decoded Error frame.

type Frame

type Frame struct {
	// Kind says which payload arrived, and therefore which of the fields
	// below is populated.
	Kind FrameKind

	// SessionID is the sixteen server-minted bytes every frame carries in both
	// directions. It is the same on every frame of one connection, which is
	// what makes a patch captured in isolation resolvable.
	SessionID []byte

	// Bytes is the encoded length of the frame as it arrived: the WebSocket
	// message payload, and therefore the bytes on the wire apart from the 2-14
	// byte RFC 6455 header. The library disables compression, so it is not a
	// compressed length. "What does a full resync cost" is a question only this
	// number answers — the server's own state does not hold it.
	Bytes int

	// Patch is set for both FramePatch and FrameSnapshot: their first five
	// fields are identical and only the update field number differs, so one
	// type reads both and Kind says which arrived.
	Patch *Patch

	// Error is set for FrameError.
	Error *Error

	// AckSeq is the sequence an Ack acknowledged, and is why a no-op resync is
	// distinguishable from a resync that produced nothing.
	AckSeq uint64
}

Frame is one decoded frame, as a plain value.

The library's own generated protobuf types are what decoded it, and they stay on this side of the boundary deliberately: a spec that asserted on *pb.Frame would put the wire schema's generated Go into its import graph and make every regenerated field a compatibility event. api-surface.md §6 records that property; this type is how it is kept while still letting a spec see the wire.

func (*Frame) String

func (f *Frame) String() string

String renders a frame the way a failure message wants to read it. It is the reason Await can say what it saw instead.

type FrameKind

type FrameKind string

FrameKind names the payload a frame carried.

It is a string rather than the schema's oneof discriminator because a failure message is the main thing it is read in, and "snapshot" reads better in one than 9 does.

const (
	FramePatch     FrameKind = "patch"
	FrameSnapshot  FrameKind = "snapshot"
	FrameError     FrameKind = "error"
	FrameHeartbeat FrameKind = "heartbeat"
	FrameAck       FrameKind = "ack"
	FrameOther     FrameKind = "other"
)

The frame kinds. FrameOther is a payload this view does not model, which is a frame arriving that this package was not updated for rather than an error: a spec asserting on kinds should say so and fail, not be silently satisfied.

type Origin

type Origin struct {
	// Kind is the category of cause: 1 a client event, 2 an effect, 3 a timer,
	// 4 a pubsub delivery, 5 the mount, 6 a resync. 0 means the frame named no
	// category, which the server never emits.
	Kind int32

	// EventID is the server-minted identity of the event that caused this
	// patch, zero when the server started the transition itself. It is the
	// authoritative causal root: unlike ClientRef, no client can name it.
	EventID uint64

	// ClientRef is the client's own correlation handle for the event, echoed
	// back unchanged. It is the one value in this struct that untrusted input
	// chose, and it exists so a browser can match a patch to the interaction
	// it sent before any server identifier reaches it.
	ClientRef uint64

	// Source is the human-readable cause, such as "event:counter.increment" or
	// "timer:slow_client". It is the field a failure message quotes.
	Source string

	// Contributing lists the events whose state changes this patch carries but
	// which were not individually patched, because coalescing collapsed them.
	// Empty is the ordinary case; a non-empty list is the evidence that
	// coalescing kept its provenance rather than discarding it.
	Contributing []uint64
}

Origin is a decoded Origin: what caused this patch.

Kind and the contributing identifiers are plain integers rather than the schema's generated enum, for the reason Frame gives. The values are in proto/gotthlive/v1/frame.proto, which is the artifact an operator holding a capture reads.

type Patch

type Patch struct {
	// ServerSeq is the frame's position in this session's outbound order,
	// monotonic from 1. It is what an Ack acknowledges and what a gap in the
	// sequence is detected against, so a spec asserting "nothing was skipped"
	// asserts on this.
	ServerSeq uint64

	// PatchID names this emitted frame, one per Patch or Snapshot. It is the
	// identifier a client's apply-latency telemetry reports back.
	PatchID uint64

	// TransitionID names the reducer invocation this frame came out of, one
	// per invocation including a transition that changed nothing. Two patches
	// sharing a TransitionID were produced by one event.
	TransitionID uint64

	// StateVersion rises if and only if the transition changed state, so it
	// distinguishes a re-render from a state change. protocol.md §4.1.
	StateVersion uint64

	// Origin says what caused this frame — which is the whole provenance
	// question a wire capture is read to answer.
	Origin Origin

	// Updates is the rendered markup, one entry per region this frame carries.
	// A region absent here was not re-rendered, which is the independent-live-
	// regions property stated as data.
	Updates []Update

	// SupersededFrom and SupersededThrough are the resync supersession edge,
	// zero on a session's first snapshot. Without them nobody can say which
	// patches the markup a user is looking at replaced.
	SupersededFrom uint64

	// SupersededThrough is the inclusive upper end of that range: the last
	// server_seq this snapshot replaced. With SupersededFrom it is the only
	// record that the superseded patches ever existed, since they were emitted,
	// counted and then dropped.
	SupersededThrough uint64

	// The session parameters a snapshot carries, zero on a patch.
	HeartbeatIntervalMS uint32

	// MaxInboundFrameBytes is the largest frame the server will accept from
	// this client, announced once so the client needs no configuration of its
	// own. Zero on a patch.
	MaxInboundFrameBytes uint32

	// AckWindow is how many unacknowledged patches the server will hold before
	// it stops emitting, which is the backpressure threshold a slow-client
	// spec measures against. Zero on a patch.
	AckWindow uint32
}

Patch is a decoded Patch or Snapshot.

func (*Patch) Fragment

func (p *Patch) Fragment(id string) (string, bool)

Fragment returns the markup this patch carries for one region, and whether it carried any at all.

"Did this patch touch the controls" is the question the independent-regions property is decided by, and the two-value form is why it can be asked: a patch carrying an empty region and a patch carrying no region are different answers.

func (*Patch) FragmentIDs

func (p *Patch) FragmentIDs() []string

FragmentIDs returns the regions this patch carries, in wire order.

func (*Patch) HTMLBytes

func (p *Patch) HTMLBytes() (int, map[string]int)

HTMLBytes returns the total rendered markup this patch carries and the per-fragment split. "A snapshot costs N bytes" is more useful beside "and here is which region spent them".

type Update

type Update struct {
	// FragmentID is the live region this markup belongs to — the ID the
	// application gave the fragment, and the value of its data-gotth-region
	// attribute.
	FragmentID string

	// HTML is the region's complete rendered markup. There is no server-side
	// diff: the diff happens in the browser, against the live DOM.
	HTML string
}

Update is one decoded FragmentUpdate.

Jump to

Keyboard shortcuts

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