updatecore

package
v0.5.5 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package updatecore is the transport-free half of the partial-update surface.

The entries here read a request and write nothing through it, so what they need from a transport is a Reader and not a request type. Both HTTP runtimes shell this package: htmlupdate over *http.Request, and the fasthttp runtime over *fasthttp.RequestCtx. One implementation answers both, which is what keeps a wire contract from being agreed twice.

Nothing here is imported by an application. Each shell redeclares Options and Response so its entries can be methods, and converts; the declarations are identical by construction, because a drifted field stops the conversion from compiling.

Index

Constants

View Source
const DefaultCSRFFieldName = "_csrf"

DefaultCSRFFieldName is the hidden field generated forms carry. It matches the generator's own default, because the two have to agree: one writes the field and the other reads it.

View Source
const DefaultCSRFHeaderName = "X-CSRF-Token"

DefaultCSRFHeaderName is where the runtime puts the token. Unlike the render and manifest headers it does not follow HeaderPrefix, because this one is a name middleware already looks for rather than a namespace this module owns.

View Source
const DefaultDataAttributePrefix = "tb"

DefaultDataAttributePrefix names the attributes the protocol puts in a document. It matches the generator's own default, because the two have to agree: one writes the attributes and the other reads them.

View Source
const DefaultGlobalName = "tinybind"

DefaultGlobalName is what the browser runtime is installed under.

View Source
const DefaultHeaderPrefix = "X-Tinybind"

DefaultHeaderPrefix names the request and response headers.

View Source
const DefaultMaxManifestBytes = 8 << 10

DefaultMaxManifestBytes bounds the validators a request may carry. Beyond it the hints are dropped, which costs bytes in the response instead of risking a proxy rejecting the request.

View Source
const DefaultMaxQueryBytes = 4 << 10

DefaultMaxQueryBytes bounds the arguments a redraw may carry, since a GET puts every one of them in the URL. Options.MaxQueryBytes overrides it, for a deployment behind a proxy with its own URL limit.

View Source
const DefaultPathPrefix = "/_tb"

DefaultPathPrefix is the URL namespace holding every framework-owned endpoint. Keeping them under one prefix means a deployment can route, cache, or protect the whole surface with one rule.

View Source
const DefaultRuntimeFileName = "tinybind"

DefaultRuntimeFileName names the served runtime file.

View Source
const DefaultStreamContentType = "application/x-ndjson; charset=utf-8"

DefaultStreamContentType marks a delta delivered as a record stream. One JSON record per line, which is the framing the module already uses for streamed values. Options.StreamContentType overrides it.

View Source
const RuntimeContentType = "text/javascript; charset=utf-8"

Variables

View Source
var BuildID = sync.OnceValue(func() string {
	if info, ok := debug.ReadBuildInfo(); ok {
		var revision string
		var modified bool
		for _, setting := range info.Settings {
			switch setting.Key {
			case "vcs.revision":
				revision = setting.Value
			case "vcs.modified":
				modified = setting.Value == "true"
			}
		}
		if revision != "" && !modified {
			return revision[:min(len(revision), 16)]
		}
	}
	return processID()
})

BuildID identifies the running binary.

It is the third and last identity in this design, and it does the job the other two deliberately do not:

  • the protocol version names the wire contract, and must stay stable across builds or every deploy would make an already-loaded page incompatible
  • a component kind names a component, and must stay stable so an unrelated deploy does not invalidate its endpoint
  • the build id names this binary, so anything that could change rendering invalidates client state: a template, a Go function a template calls, the render runtime itself, or a dependency

A component kind cannot do this job. It hashes one component's own compiled plan, so it misses a change in a component that one calls, in an external function, and in the framework's own rendering.

The value comes from the version control revision the binary was stamped with. A binary built from a dirty tree, or with no stamping at all, gets a value unique to the process instead: during development every restart should invalidate, and guessing otherwise would serve stale regions while editing.

View Source
var ErrCSRFMismatch = errors.New("htmlupdate: CSRF token does not match the session")

ErrCSRFMismatch reports a token that is not the session's.

View Source
var ErrCSRFMissing = errors.New("htmlupdate: request carries no CSRF token")

ErrCSRFMissing reports an unsafe request carrying no token at all.

Functions

func DecodeManifest

func DecodeManifest(encoded string) delta.Manifest

DecodeManifest reads the compact validator list a client sends back. The encoding is "id:frame" or "id:frame:children" separated by commas, which stays inside one header and needs no escaping because every part is an opaque token.

The third part is what lets a list say its rows moved without its parent being replaced. It is absent for a boundary containing no nested boundary, which is most of them, and a pair with only two parts still reads.

func EncodeManifest

func EncodeManifest(manifest delta.Manifest) string

EncodeManifest renders the validator list a client sends back. It exists so a test, and any non-browser client, can produce exactly what the runtime does.

func MatchesETag

func MatchesETag(header, etag string) bool

matchesETag reads an If-None-Match header, which is a comma-separated list of tags or the wildcard, and reports whether the response's tag is in it.

A weak comparison is the right one here: the two representations differ only if the bytes differ, so the weak prefix carries no extra information. MatchesETag reports whether an If-None-Match header names etag. It is exported because each shell answers a conditional request for the runtime asset itself, and the comparison has to be the one the entries use.

func QueryBool

func QueryBool(values url.Values, name string, target *bool) error

QueryBool decodes a bool parameter.

func QueryFloat

func QueryFloat(values url.Values, name string, target *float64) error

QueryFloat decodes a float parameter.

func QueryInt

func QueryInt(values url.Values, name string, target *int) error

QueryInt decodes an int parameter.

func QueryOptional

func QueryOptional[T any](values url.Values, name string, target **T) error

QueryOptional decodes a parameter the template declared optional. An absent name is the absent value; a present but undecodable one is still an error.

func QueryString

func QueryString[T ~string](values url.Values, name string, target *T) error

QueryString decodes any string-kinded parameter, covering plain strings, decimals, and generated enums.

func QueryTime

func QueryTime(values url.Values, name string, target *time.Time) error

QueryTime decodes an instant, date, or time parameter in RFC 3339 form, which is the form CanonTime writes and the one a query string can carry unambiguously.

func QueryURL

func QueryURL(values url.Values, name string, target *url.URL) error

QueryURL decodes a URL parameter.

func RenderToken

func RenderToken(mode Mode, version int) string

RenderToken is the value a response echoes for one mode.

The version is the one the request carried, not one this package chose. A caller versioning its own wire sees its own number come back; a caller that versions nothing gets a bare mode name, because inventing a number here would be this package versioning a contract it no longer owns.

func ReportStreamError

func ReportStreamError(err error)

ReportStreamError sends a post-commit failure to the installed handler.

func RuntimeSource

func RuntimeSource() []byte

RuntimeSource is the browser runtime this package implements.

The bytes are the point. A framework that already ships a runtime cannot put two on one document — that would be two boundary id spaces, two build identities, and two script tags with nothing deciding which owns a region — so it merges ours into its own asset. Without readable bytes, merging means keeping a copy, and a copy is not a version-pinned dependency: it drifts on upgrade with nothing in the build failing, and a drifted browser runtime is a silently dead page rather than a compile error.

The bytes carry no naming choice. The runtime reads its attribute prefix, header namespace, endpoint prefix, and installed name from the configuration it is given, so one asset serves every deployment and merging it needs no build step. RuntimeConfig produces that configuration; the file installs createPartialUpdateRuntime for a caller constructing an instance directly.

func RuntimeVersion

func RuntimeVersion() string

RuntimeVersion is the content identity of the browser runtime. It appears in the runtime path so a new build gets a new URL.

func SendsValues

func SendsValues(sequences bool, operation delta.Operation) bool

SendsValues decides which half of a fragment travels, for every path that sends one.

Values replace the markup only when they are smaller. A fragment of two elements costs more as an address plus its values than as the markup itself, because the address is per-operation overhead and there is almost no static text to save; a list row is exactly that shape, and its parent — a hundred hole frames — is the opposite one. Choosing per fragment is what keeps the split from ever being a loss.

It is one function because the buffered path applied the size test and the streamed path did not, so the claim held on one path and not on its sibling — and the streamed path is the one every navigation goes through. That is the third defect of this shape: a rule applied on one path and not the other. A predicate with one home is what stops there being a fourth.

func SetStreamErrorHandler

func SetStreamErrorHandler(fn func(error))

SetStreamErrorHandler installs the destination for stream failures raised after the response committed.

Types

type Asset

type Asset struct {
	// Source is the file's content.
	Source []byte
	// Version is the content digest. Two builds with the same digest are the
	// same file, and a changed digest is a changed URL.
	Version string
	// ContentType is the media type the file must be served as.
	ContentType string
	// FileName is the name this package would serve it under, which a caller
	// serving it elsewhere may ignore.
	FileName string
}

Asset is one static file this package requires a page to load.

The module decides what the bytes are and what identifies them; the caller decides where they are served, under what name, and with what cache policy.

type DeltaInstance

type DeltaInstance struct {
	ID    string `json:"id"`
	Frame string `json:"frame"`
	// Children digests the nested boundary ids, so a later request can say a
	// list reordered without its parent being replaced to express it. Absent for
	// a boundary containing no nested boundary, which is most of them.
	Children string `json:"children,omitempty"`
	// Parent names the enclosing boundary, so a region that disappears can be
	// attributed to the boundary that will report the survivors. Absent for an
	// outermost boundary.
	Parent string `json:"parent,omitempty"`
}

type DeltaOperation

type DeltaOperation struct {
	Kind string `json:"kind"`
	ID   string `json:"id"`
	HTML string `json:"html"`
	// Boundaries names the nested boundaries appearing as holes in HTML.
	//
	// A hole whose id also carries an operation in this response is filled from
	// it; one that does not is a region the client already holds, and it moves
	// that live node in rather than recreating it — which is what keeps the
	// focus, the form values, and the media state inside it. Without the list a
	// missing fragment would be indistinguishable from a truncated response.
	Boundaries []string `json:"boundaries,omitempty"`
	// Seq addresses this fragment's static half and Values are the varying half
	// a client walks it with. They replace HTML for a client that said it can
	// walk sequences, because the statics then travel once per client instead of
	// once per render.
	Seq    string   `json:"seq,omitempty"`
	Values []string `json:"values,omitempty"`
}

type DeltaResponse

type DeltaResponse struct {
	Operations []DeltaOperation `json:"ops"`
	Manifest   []DeltaInstance  `json:"manifest,omitempty"`
	// Head is the merged head of the new composition, sent so the client can
	// install what a newly reachable component contributed before its markup
	// lands and flashes unstyled.
	Head []string `json:"head,omitempty"`
	// Navigate asks the browser to leave the page, which an action uses when
	// it changed where the user belongs.
	Navigate string `json:"navigate,omitempty"`
	// Live says the composition this response describes owns a live boundary, so
	// a client that applied it should open a live request. It is the handoff
	// marker of rule:stream-termination-marker on the buffered path.
	//
	// Absent means no live boundary, and a page that has none is what it was
	// before this field existed: a client that reads no marker issues no
	// speculative request and costs the server no page execution.
	Live bool `json:"live,omitempty"`
}

DeltaResponse is the JSON body of a navigation or action response.

It carries no version field. Nothing compared the one it used to carry, and a field nothing compares is not a version but a constant every response asks the wire for and every client ignores. A caller versioning its own wire adds its own field beside this shape.

type DeltaStream

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

DeltaStream is an open record stream a producer writes boundary completions to as they settle.

It exists so the transport and the producer stay separate. A synchronous delta drives it today; an asynchronous render sequence drives it by calling Replace once per completion, which makes wiring one in a call rather than a redesign.

func (*DeltaStream) Children

func (s *DeltaStream) Children(instanceID string, entry ManifestEntry, boundaries ...string)

Children writes a boundary whose own markup is unchanged and whose nested boundaries are now these, in this order.

It carries no markup, which is the point: appending one row to a list costs the list of ids rather than the list of holes. A client keeps what the list keeps, moving what moved, drops what it omits, and fills what arrives as its own operation in the same response.

func (*DeltaStream) Close

func (s *DeltaStream) Close() error

Close writes the terminator. Without it the client treats the stream as truncated and discards its manifest, so a producer must always reach here.

The terminator names which ending this is: a navigation says whether a live request should follow, and a live stream says whether the client should come back. A close with no reason would make a healthy lifetime rollover indistinguishable from a fault, so a client would back off on both and stall a working screen every time the server rotates a connection.

func (*DeltaStream) ExpectLive

func (s *DeltaStream) ExpectLive()

ExpectLive marks this stream's terminator as handing off to a live request, which a navigation does when the route it just described owns a live boundary.

Without it a client either opens a speculative live request on every navigation — one full page execution per screen that will never deliver anything — or the caller hardcodes which routes are live.

func (*DeltaStream) Fail

func (s *DeltaStream) Fail(message string)

Fail reports a failure that happened after the response committed. The status is already sent, so this is the only way to say so.

func (*DeltaStream) Replace

func (s *DeltaStream) Replace(instanceID, html string, entry ManifestEntry, boundaries ...string)

Replace writes one settled boundary, the validator it produced, and the nested boundaries appearing as holes in its markup.

A hole whose id also arrives as an operation on this stream is filled from it; one that does not is a region the client already holds and moves in. The list is what separates the two, since nothing in the markup does.

func (*DeltaStream) ReplaceValues

func (s *DeltaStream) ReplaceValues(instanceID, sequence string, values []string, entry ManifestEntry, boundaries ...string)

ReplaceValues is Replace for a client that walks sequences: the fragment travels as the address of its static half and the values that fill it, so the statics cost one response per client rather than one per render.

func (*DeltaStream) Retry

func (s *DeltaStream) Retry(after time.Duration) error

Retry closes a healthy stream the server chose to end: a lifetime bound, a shutdown, a rebalance. The client reconnects promptly instead of backing off, because nothing failed.

after is the server's own hint for how long to wait. Zero leaves the delay to the client, which is the right answer for an ordinary rollover; a server shedding load or rolling a deploy is the only party that knows to spread the return, so it is the only one that can fill this in.

Calling it on a navigation stream is a mistake this package does not guard against, because a navigation has nothing to reconnect to; it is here for the live path, where a bare close cannot mean stop.

func (*DeltaStream) Sent

func (s *DeltaStream) Sent(instanceID string) bool

Sent reports whether an instance already appeared, so a producer emitting completions out of order does not restate one it already wrote.

func (*DeltaStream) Settled

func (s *DeltaStream) Settled(boundaryID string, html []byte)

Settled writes an await boundary that finished after the initial pass.

It addresses a placeholder inside a region the client already installed, which is a different namespace from an instance id, so it is its own record kind rather than an operation with a surprising target.

func (*DeltaStream) Signal added in v0.5.3

func (s *DeltaStream) Signal(name string, payload []byte)

Signal writes an instruction a live source emitted beside its deliveries.

It replaces nothing on screen, so it names no instance and advances no validator. The client looks the name up in the table it registered while the page loaded and calls what it finds; nothing here is code, which is what lets a page keep a script-src with no unsafe-eval and still be directed.

payload is the JSON the source already encoded, written through as it is.

func (*DeltaStream) Unchanged

func (s *DeltaStream) Unchanged(instanceID string, entry ManifestEntry)

Unchanged restates a boundary's validator without markup, so the client can rebuild its whole manifest from what it received.

type Failure

type Failure struct {
	// Kind is why the request was refused.
	Kind FailureKind
	// Status is the response status this package would have written.
	Status int
	// Message is the plain-text body this package would have written. It never
	// contains anything the request supplied, so it is safe to send as is.
	Message string
	// Err is the underlying cause, when there is one. A decoder rejection and a
	// render failure carry theirs; a stale page has none, because nothing
	// failed.
	//
	// It may name internal detail, so it belongs in a log rather than in a
	// response body.
	Err error
	// KindID and InstanceID name what was asked for, when the path was
	// well-formed enough to say. Both are attacker-supplied, so treat them as
	// untrusted when they reach a log.
	KindID     string
	InstanceID string
}

Failure is one request an update endpoint could not answer.

This package has to write a response, because it owns the endpoint. It does not have to decide what a failure looks like, which is why the whole value reaches the caller instead of a status and a line of plain text reaching the client. A caller with problem responses, its own error pages, a request-scoped logger, or a tracer sees every one of these.

func (Failure) Error

func (f Failure) Error() string

Error makes a Failure usable wherever an error is, so a caller can hand it straight to a logger or a span.

func (Failure) Unwrap

func (f Failure) Unwrap() error

Unwrap exposes the cause, so errors.Is and errors.As reach it.

type FailureKind

type FailureKind int

FailureKind names why an update endpoint could not answer.

The kind is what a caller branches on. The status and the message are defaults it may keep or replace, but the kind is the fact: a stale page and a failed render are the same 4xx-or-5xx to a proxy and completely different events to whoever is on call.

const (
	// FailureMalformedRequest is a redraw that named no component: the mode said
	// redraw and the kind or instance header was missing or empty.
	FailureMalformedRequest FailureKind = iota
	// FailureUnknownComponent is a kind this deployment does not publish.
	//
	// It is the ordinary version-skew signal: a page loaded before a deploy
	// asks for a component whose markup has since changed, gets a 404, and
	// reloads. A sustained rate of it after a deploy has settled means
	// something else.
	FailureUnknownComponent
	// FailureArgumentsTooLarge is a query past the configured bound.
	FailureArgumentsTooLarge
	// FailureInvalidArguments is a query the generated decoder refused.
	FailureInvalidArguments
	// FailureRenderFailed is a component that could not render.
	FailureRenderFailed
)

func (FailureKind) String

func (k FailureKind) String() string

String names the kind for a log line or a span attribute.

type ManifestEntry

type ManifestEntry struct {
	Frame    string
	Children string
	Parent   string
}

ManifestEntry is what a client stores for one instance beside its markup: the validator of the region's own bytes, the digest of its nested boundary ids, and the boundary enclosing it. All three travel on every operation record, because a client rebuilding its manifest from a stream has no other source for them and the next request is compared against all three.

type Mode

type Mode int

Mode is the rendering a request asked for.

const (
	// ModeDocument is the complete HTML document. It is what a request without
	// a usable render header gets, including one from an incompatible client.
	ModeDocument Mode = iota
	// ModeNavigation returns only the changed boundaries of the same route.
	ModeNavigation
	// ModeLive returns the deliveries of the same route's live boundaries, on a
	// response held open for as long as the subscriptions live.
	//
	// It is its own mode rather than a navigation held open because the two
	// differ in duration and in termination: a navigation ends when the route has
	// been described, a live response ends when every source finishes or when the
	// server reaches a lifetime bound. Sharing one name meant a deployment could
	// not route, time out, or bound them separately, and a served-mode log could
	// not tell an hours-long subscription from ordinary navigation traffic.
	ModeLive
	// ModeRedraw returns one registered component's subtree, addressed by the
	// kind and instance headers rather than by the URL path.
	//
	// It is a request mode so a caller can answer a redraw at any URL it likes.
	// Usually that is the page the component sits on, where the redraw inherits
	// the page's own authorization rather than needing a second path pattern kept
	// in step with the one protecting the page — two rules that must agree and
	// that nothing forces to agree.
	ModeRedraw
	// ModeSequence returns the static half of one fragment, addressed by a
	// digest of its own content.
	//
	// It is the one response in this package that is not per user: a sequence
	// derives from the template rather than from a request, so it is the only
	// one that can be public, immutable, and held by a shared cache.
	ModeSequence
)

type Negotiated

type Negotiated struct {
	Mode Mode
	// Version is whatever the client wrote after "v=" in the render header, or
	// zero when it wrote none. This package neither defines it nor compares it:
	// the browser client belongs to the caller, so the caller owns its wire
	// version and what a mismatch means. It is carried so a caller that does
	// version its wire can read it, and it is echoed back on the response.
	//
	// The compatibility axis this package still operates is the build identity,
	// whose value Options.BuildID already makes the caller's.
	Version int
	// Known holds the validators the client already has. It is empty on a
	// client's first update, which simply yields a larger delta.
	Known delta.Manifest
}

Negotiated is what a request asked for, after validation.

type Options

type Options struct {
	// Key authenticates validators. Two renders that are to be compared must
	// use the same key; rotating it forces complete documents, which is the
	// intended effect of a rotation.
	//
	// An unkeyed digest of low entropy content lets anyone confirm a guess by
	// comparing digests, so a deployment serving non-public pages must set it.
	Key []byte
	// HeaderPrefix overrides the header namespace. Empty uses
	// DefaultHeaderPrefix. The runtime reads it from its configuration, so
	// overriding it needs no rebuilt runtime.
	HeaderPrefix string
	// DataAttributePrefix overrides the attribute namespace. Empty uses
	// DefaultDataAttributePrefix.
	//
	// It must match the generator's own DataAttributePrefix, because that is
	// what wrote the instance attributes into the markup this runtime reads.
	// It also names the preserve and ignore markers an application author
	// writes by hand, which is why a framework needs to own it: those are the
	// author's surface, not a wire detail.
	DataAttributePrefix string
	// GlobalName overrides the name the browser runtime is installed under.
	// Empty uses DefaultGlobalName.
	//
	// A framework sets it so its users call the framework's own name rather
	// than a dependency's.
	GlobalName string
	// RuntimeFileName overrides the base name of the served runtime asset.
	// Empty uses DefaultRuntimeFileName. The content digest and the .js suffix
	// are appended either way, so the URL stays immutably cacheable.
	RuntimeFileName string
	// CallerOwnsRuntime says the caller ships its own browser runtime, so this
	// package serves and references none: Mount registers no asset route and
	// ScriptTag returns nothing.
	//
	// Usually that runtime is this one, merged into a larger asset from
	// RuntimeSource. Two runtimes on one document would mean two boundary id
	// spaces and two build identities, so a framework that already has one takes
	// this rather than adding a second.
	CallerOwnsRuntime bool
	// ServeRuntime asks this package to serve the reference browser runtime at a
	// content-hashed URL and to write the script tag that loads it.
	//
	// It is off by default, which is the whole of the difference from earlier
	// versions: the browser half belongs to the caller, so serving one is
	// something a deployment asks for rather than something it inherits.
	//
	// Exactly one of this and CallerOwnsRuntime must be set, and Validate says so
	// at startup. A build that set neither would compile and then serve pages
	// that silently stop updating, which is the worst failure shape available
	// here: nothing fails, the page is just quietly dead.
	ServeRuntime bool
	// CSRFFieldName is the hidden field generated forms carry. Empty uses
	// DefaultCSRFFieldName.
	//
	// It must match the generator's own CSRFFieldName, because that is what
	// wrote the field into the markup this reads back: one writes it and the
	// other reads it, and nothing links the two at compile time.
	CSRFFieldName string
	// CSRFHeaderName overrides the header the browser runtime puts the CSRF
	// token in. Empty uses DefaultCSRFHeaderName.
	//
	// It does not follow HeaderPrefix on purpose: the render, manifest, and
	// build headers name this protocol, and this one names a convention every
	// framework's middleware already looks for.
	CSRFHeaderName string
	// PathPrefix overrides the URL namespace of every framework endpoint.
	// Empty uses DefaultPathPrefix. Unlike the header names, the runtime learns
	// this one at load time, so overriding it needs no rebuilt runtime.
	PathPrefix string
	// BuildID overrides the identity of the running binary. Empty uses
	// BuildID(), which is the version control revision the binary was stamped
	// with, or a per-process value when the tree was dirty or unstamped.
	//
	// A page rendered by a different build has client state this binary cannot
	// vouch for, so it is served a complete document instead of a delta.
	BuildID string
	// MaxManifestBytes caps the manifest header a request may carry. Zero uses
	// DefaultMaxManifestBytes. An oversized manifest is ignored rather than
	// rejected, so the response is a larger delta instead of an error.
	MaxManifestBytes int
	// MaxQueryBytes caps the arguments a redraw may carry. Zero uses
	// DefaultMaxQueryBytes. Unlike an oversized manifest an oversized query is
	// rejected, because the arguments are the request rather than a hint.
	MaxQueryBytes int
	// StreamContentType overrides the media type of a streamed delta. Empty
	// uses DefaultStreamContentType.
	//
	// This one names the wire format rather than a limit, so overriding it is a
	// framing choice a client has to agree with, not a tuning knob.
	StreamContentType string
	// OnFailure receives every request an endpoint of this package could not
	// answer. It observes rather than answers.
	//
	// Every entry returns the response it computed, with its Failure field set,
	// so a caller with its own error pages substitutes them by sending something
	// else instead of what it was handed. This hook is for the log line and the
	// span, which a caller wants on every refusal whether or not it changes the
	// answer — and which are otherwise lost, since a status alone cannot say
	// whether a page was stale or a render failed.
	//
	// It takes the request's context rather than the request. A log line and a
	// span both want the trace, the deadline, and whatever the caller's own
	// middleware put there, and none of them want the transport; taking the
	// narrower value is also what lets this field mean the same thing on a
	// backend whose request type is not *http.Request.
	OnFailure func(ctx context.Context, failure Failure)
}

Options configure one set of update endpoints.

func (Options) AttributePrefix

func (o Options) AttributePrefix() string

func (Options) Build

func (o Options) Build() string

Build returns the configured identity, or the running binary's.

func (Options) BuildHeader

func (o Options) BuildHeader() string

func (Options) CSRFField

func (o Options) CSRFField() string

func (Options) CSRFHeader

func (o Options) CSRFHeader() string

func (Options) CSRFToken

func (o Options) CSRFToken(r Reader) string

CSRFToken reads the token a request carries, header first and form body second.

The order is not arbitrary. The header is what the runtime sends and the only channel a non-form body has; the field is the fallback for a submission made without script. Reading the header first also means an ordinary fetch never pays for parsing a body it does not have.

Reading the field consumes the request body through ParseForm, as any handler reading a form does.

func (Options) ContentTypeFor

func (o Options) ContentTypeFor(mode Mode) string

ContentTypeFor names the body a buffered entry writes in each mode.

A live request reaching a buffered entry is answered with the document, since that entry cannot hold a delivery stream open, so it takes the document type rather than the stream one. StreamHeaders is what an entry that can hold one uses instead.

func (Options) Global

func (o Options) Global() string

func (Options) HeaderNamespace

func (o Options) HeaderNamespace() string

func (Options) Headers

func (o Options) Headers(r Reader, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment) http.Header

Headers is what a response to this request must carry, for the entries that write their body directly and therefore need their headers set first.

It is computable before anything renders: the Vary axes come from which request headers this package reads, the content type from the mode negotiated, and the live marker from the composition. Pass the wrappers and leaf a render entry will be given; pass none and the live marker is omitted.

func (Options) InstanceHeader

func (o Options) InstanceHeader() string

func (Options) KindHeader

func (o Options) KindHeader() string

kindHeader and instanceHeader name the component a redraw addresses.

They are headers rather than path segments so a redraw can be answered at any URL, which is what lets a caller serve one from the page the component sits on. There the redraw inherits the page's own authorization; on a reserved path it needs a second path pattern kept in step with the first, and nothing forces two such rules to agree.

They are headers rather than query parameters because the generated decoder treats an unknown parameter name as an error, so a query-carried kind and instance would reserve two names an author could then not declare.

func (Options) LiveHeader

func (o Options) LiveHeader() string

liveHeader names the response header saying whether this composition owns a live boundary.

func (Options) LiveHeaders

func (o Options) LiveHeaders(r Reader, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment) http.Header

LiveHeaders is StreamHeaders for the entry that does hold subscriptions open, so a live request keeps the live mode rather than being downgraded.

func (Options) ManifestHeader

func (o Options) ManifestHeader() string

func (Options) ManifestLimit

func (o Options) ManifestLimit() int

func (Options) Negotiate

func (o Options) Negotiate(r Reader) Negotiated

Negotiate resolves how a request must be answered.

Anything unrecognized resolves to ModeDocument rather than to an error: a stale client, a truncated header, and a proxy that dropped a header must all still produce a working page. That is a total function on the mode name rather than a version comparison, so it holds with no version at all.

func (Options) OpenStream

func (o Options) OpenStream(w io.Writer, plan StreamPlan) *DeltaStream

OpenStream writes the head record and returns the stream the records go to.

Everything that could change the status has to be decided before this call, because after it the status is fixed and a failure can only be reported in band through Fail.

func (Options) OperationBody

func (o Options) OperationBody(operation delta.Operation, sequences bool) DeltaOperation

OperationBody writes one operation, in whichever half the client can use.

func (Options) PathNamespace

func (o Options) PathNamespace() string

PathNamespace returns the endpoint namespace without a trailing slash.

func (Options) PlanBufferedStream

func (o Options) PlanBufferedStream(r Reader, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment, options []htmlbind.Option) (StreamPlan, error)

PlanBufferedStream is PlanStream for the entry that renders the whole delta before it writes anything, so every failure it can have is pre-commit.

It serves the navigation mode only. Anything else is planned as a document, because a buffered render cannot hold a delivery open.

func (Options) PlanStream

func (o Options) PlanStream(r Reader, serveLive bool, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment, options []htmlbind.Option) (StreamPlan, error)

PlanStream decides everything a streamed render can decide before it writes.

serveLive says whether the caller's entry holds subscriptions open. A live request reaching one that does not is planned as a navigation and terminated, so a client that opened a live connection to a route this caller does not serve live learns so at once instead of holding a connection that will never deliver.

A failure here is an ordinary error: nothing is written yet, so the caller can still choose a status and serve an error page. That window exists on both transports, because this runs while the handler still owns the request.

func (Options) QueryLimit

func (o Options) QueryLimit() int

func (Options) Redraw

func (o Options) Redraw(r Reader, reg *Registry, options ...htmlbind.Option) (Response, bool)

Redraw answers a redraw request at whatever URL the caller serves it from, and reports whether it did.

It is the entry a caller branches on inside its own page handler:

func page(w http.ResponseWriter, r *http.Request) {
	ApplyTo(options.RedrawHeaders(r), w)
	if answer, ok := options.Redraw(r, registry); ok {
		w.Header().Set("Cache-Control", "private, no-cache")
		_, _ = answer.WriteTo(w)
		return
	}
	// ordinary page render
}

The vary axes go on before the branch, because a page and the redraws of the components on it share this URL and a cache has to key on them whichever way the request turns out. Everything after the branch — the cache policy, and whether to answer a conditional request with 304 — is the caller's; see Response for what this package computes and what it leaves alone.

Addressing it at the page's own URL is the point. Path protection is configured by path pattern, so a redraw on a reserved path needs its own pattern maintained in parallel with the one protecting the page the component sits on — two rules that must agree and that nothing forces to agree. At the page URL the redraw inherits that protection automatically, and placed after the handler's own checks it inherits those too, not merely the middleware's.

A request that is not a redraw returns false with nothing written, including a request from a page another build rendered: at a page URL the right answer to a stale redraw is that page, which the caller is about to render anyway, and that costs a reload rather than a refusal and then a reload.

options reach the component's render, so a redraw sees the same cache store, URL scheme policy, and CSRF token the page render was given. Without them a component renders one way inside its page and another in the response that replaces it — and one containing an unsafe form does not render at all, since htmlbind.Builder.CSRFField needs a token. The boundary prefix and the build identity are supplied from these Options and do not need passing.

func (Options) RedrawHeaders

func (o Options) RedrawHeaders(r Reader) http.Header

RedrawHeaders names the Vary axes a URL that answers redraws depends on, whichever way this request turns out.

A page handler declares them before it branches: a page and the redraws of the components on it share one URL, so a cache that learned only the page would answer a redraw from it, and one that learned only one component's redraw would answer another component with it. The axes have to be on the response whether or not this request was a redraw, which is why they are computable without deciding.

func (Options) Refuse

func (o Options) Refuse(r Reader, f Failure) Response

Refuse is FailureResponse for the entries, which also report the refusal through the observation hook when a caller installed one and give the refusal the vary axes an answer would have had.

A refusal is a response at the same URL as everything else, and some refusals are cacheable: 404 is heuristically cacheable with no Cache-Control at all, so a stored redraw 404 carrying no Vary can be handed to a request for the page. It reads the same headers a successful redraw reads, so it varies on the same ones.

func (Options) Render

func (o Options) Render(w io.Writer, r Reader, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment, options []htmlbind.Option) error

Render answers one request with either a complete document or a delta.

It buffers: the delta path encodes one JSON body and the document path writes the collected chain, so nothing is held open and every failure is an ordinary error. That is why this entry needs no inversion on either transport — the report that grouped it with the streaming entries was reading the name rather than the writes.

func (Options) RenderDocument

func (o Options) RenderDocument(w io.Writer, plan StreamPlan, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment) error

RenderDocument writes the complete document, which is what a request with no usable render header gets and what every stream entry falls back to.

The document render collects so every boundary carries its instance attribute; without them a later delta could not find its targets.

func (Options) RenderHeader

func (o Options) RenderHeader() string

func (Options) RenderOptions

func (o Options) RenderOptions(caller []htmlbind.Option) []htmlbind.Option

func (Options) RunBufferedStream

func (o Options) RunBufferedStream(stream *DeltaStream, plan StreamPlan) error

RunBufferedStream writes an already-rendered delta as records.

func (Options) RunStream

func (o Options) RunStream(ctx context.Context, stream *DeltaStream, plan StreamPlan, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment) error

RunStream writes the records of a planned stream and terminates it.

It reads nothing from the request: the plan carries everything, which is what lets this run from a body stream writer on the transport that forbids touching the request there.

ctx is the caller's cancellation, not the request's. On net/http those are usually the same value; on fasthttp they cannot be, because a RequestCtx is pooled and this outlives the handler that owned it.

func (Options) RuntimeAsset

func (o Options) RuntimeAsset() Asset

RuntimeAsset is the browser runtime as a static asset, for a caller that serves its own files.

func (Options) RuntimeBaseName

func (o Options) RuntimeBaseName() string

func (Options) RuntimeConfig

func (o Options) RuntimeConfig() RuntimeConfig

RuntimeConfig is the configuration matching these options, so the server and the browser cannot disagree about a name.

It carries no CSRF token: a token belongs to a session and these options belong to the process. Use RuntimeConfigFor to add one.

func (Options) RuntimeConfigFor

func (o Options) RuntimeConfigFor(csrfToken string) RuntimeConfig

RuntimeConfigFor is RuntimeConfig carrying this session's CSRF token, so the runtime sends it on every request it issues.

The header is the channel for anything the runtime fetches; the hidden field generated into each form is the channel for a submission with no script. They carry the same value, which is what a header carrying exactly one value requires of the token: one per session.

func (Options) RuntimePath

func (o Options) RuntimePath() string

RuntimePath is the URL the browser runtime is served from. The version segment makes the response immutable, which is why the handler may set a long max-age.

func (Options) ScriptTag

func (o Options) ScriptTag() string

ScriptTag is the element loading the runtime, ready to place at the end of a document body.

The caller injects it, because this milestone has no document shell bootstrap. The tag carries the whole runtime configuration, so one shared asset works for any set of names without being rebuilt.

A caller owning the runtime gets an empty string: a tag pointing at an asset this build does not serve is worse than no tag at all.

func (Options) ScriptTagFor

func (o Options) ScriptTagFor(csrfToken string) string

ScriptTagFor is ScriptTag carrying this session's CSRF token, which is what a handler that renders forms uses.

func (Options) Sequence

func (o Options) Sequence(r Reader) (Response, bool)

Sequence answers a request for one sequence tree, and reports whether it did.

It is the entry a caller branches on inside its own handler, exactly as Redraw is, so the address a client asks at is the caller's to choose and this package mounts nothing:

func page(w http.ResponseWriter, r *http.Request) {
	if answer, ok := options.Sequence(r); ok {
		w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
		_, _ = answer.WriteTo(w)
		return
	}
	// ordinary page render
}

The cache policy is the caller's, and a sequence is the one answer here that may be public and held forever: it is addressed by a digest of its own content, so a template edit produces a new address rather than a new body at the old one, and nothing needs invalidating.

An address this process has never rendered is answered 404, and a client falls back to asking for the assembled form it can always be sent instead. That is the whole recovery path: a sequence is an optimisation over markup that is still available, never a thing a screen depends on.

func (Options) SequenceAddressHeader

func (o Options) SequenceAddressHeader() string

sequenceAddressHeader names the address a sequence request asks for.

It spells out Address rather than sitting one letter from the capability header beside it: a pair reading -Sequences and -Sequence is two headers a reader tells apart by counting characters, which is the kind of naming that produces a bug nobody can see in a diff.

func (Options) SequenceHeader

func (o Options) SequenceHeader() string

RenderOptions carries the naming these options configure into every htmlbind entry this package drives, so the placeholder element, the boundary identifiers, and the instance attributes are one naming system rather than two. Caller options follow, so a caller can still override. sequenceHeader names the request header a client sets to say it can walk a sequence tree, so a response may send values instead of markup.

It is a capability rather than a list of held addresses: the choice between a fragment and its values is a heuristic, since values a client cannot resolve cost it one fetch and a fragment where values would have done costs a few bytes. Neither is wrong, so no per-address bookkeeping travels.

func (Options) ServesRuntime

func (o Options) ServesRuntime() bool

func (Options) StreamHeaders

func (o Options) StreamHeaders(r Reader, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment) http.Header

StreamHeaders is Headers for a streamed navigation, whose body is a record stream rather than one of the buffered shapes.

A live request reaching an entry that does not hold subscriptions open is answered as a navigation and terminated, so the echo says navigation: what a response claims to be has to be what it is, or a proxy substitution stops being detectable.

func (Options) StreamMediaType

func (o Options) StreamMediaType() string

func (Options) Validate

func (o Options) Validate() error

Validate reports every option this package cannot use, so a caller running a startup validation pass hears about all of them at once rather than finding the first one in a browser.

Nothing calls it automatically: an Options value is a struct literal, so there is no constructor to fail in, and a handler is the wrong place to discover a configuration mistake.

func (Options) VaryOn

func (o Options) VaryOn(mode Mode) []string

VaryOn names the request headers a response in this mode depends on.

The render and build headers are always there: without the first a shared cache can hand a delta body to a browser asking for a page, and without the second it can hand one build's markup to another build's client. A redraw and a sequence read more, and a page and its redraw share a URL, so two components redrawing on one page would otherwise be a single cache entry.

func (Options) VerifyCSRF

func (o Options) VerifyCSRF(r Reader, expected string) error

VerifyCSRF compares what a request carries against the session's token.

expected is the caller's to produce, from wherever its session lives. An empty expected is refused rather than treated as "nothing to check": a session lookup that quietly returned nothing would otherwise disable the whole control for exactly the requests that most need it.

This is a token check and nothing else. Origin and Fetch Metadata validation belong to middleware that sees the request before any of this — Go's own http.CrossOriginProtection is what wraps a handler with them — and they are worth having: the two defenses fail for unrelated reasons, which is the point of running both.

func (Options) WantsSequences

func (o Options) WantsSequences(r Reader) bool

WantsSequences reports whether this request said it can walk sequences.

func (Options) WantsUpdate

func (o Options) WantsUpdate(r Reader) bool

WantsUpdate reports whether the caller can apply an update response.

An ordinary form submission cannot, so a handler branches on this and redirects instead, which is what keeps a page working without JavaScript.

func (Options) WriteNavigate

func (o Options) WriteNavigate(url string) (Response, error)

WriteNavigate tells the browser to leave the page, which is how an action that changed where the user belongs stays correct without guessing which regions to rewrite.

func (Options) WriteUpdate

func (o Options) WriteUpdate(r Reader, updates []Update, options ...htmlbind.Option) (Response, error)

WriteUpdate answers a mutating request with the regions it changed, so one round trip both performs the action and refreshes the page.

The body is the same shape a redraw returns, so the browser applies it with the same code. Unlike a redraw this request is not idempotent: it carries ambient credentials, so it needs CSRF protection, and its response is never cacheable.

options reach every region's render. The token one matters most here: a region holding an unsafe form emits a CSRF field, and without a token that render fails outright — which is this entry's own headline case, since rewriting a form with its validation errors is what it exists for.

func (Options) WriteUpdateStatus

func (o Options) WriteUpdateStatus(r Reader, status int, updates []Update, options ...htmlbind.Option) (Response, error)

WriteUpdateStatus is WriteUpdate with an explicit status, so a failed validation can return 422 and still rewrite the form region with its errors.

The browser applies an update response whatever the status says, because rendering the failure is the point.

type QueryError

type QueryError struct {
	// Parameter is the declared name the request got wrong.
	Parameter string
	// Reason completes the sentence "redraw parameter <name> …", so it reads
	// the same in a log line and in a problem response.
	Reason string
}

QueryError is a redraw parameter the decoder refused, naming which one.

The name is what makes a refusal answerable: a failure response reports the parameter as a field-level error rather than one line of prose a caller would have to parse. The reason never quotes the value, because the value is attacker-supplied and the response is not the place to reflect it.

func (*QueryError) Error

func (e *QueryError) Error() string

type Reader

type Reader interface {
	// Header is the first value of the named request header, or "".
	Header(name string) string
	// Method is the request method, upper case, as both transports report it.
	Method() string
	// RawQuery is the undecoded query string, without the leading "?".
	//
	// It is separate from Query because a redraw bounds the arguments before
	// parsing them: parsing to measure would do the work the bound exists to
	// refuse.
	RawQuery() string
	// Query is the parsed query string.
	Query() url.Values
	// FormValue is the named value of a urlencoded or multipart request body.
	//
	// Only the CSRF entries read it, and only after the header channel came up
	// empty, so an ordinary fetch never pays for a body it does not have.
	FormValue(name string) string
	// Context is the request's context: its cancellation, its deadline, and
	// whatever the caller's middleware put there.
	Context() context.Context
}

Reader is everything the update entries read from a request.

It is deliberately six methods long. Each one is here because an entry in this package calls it, and the list is the measured answer to what "reads a request and writes nothing through it" costs a second transport: a header lookup, the method, the query in both its raw and parsed forms, one form value, and the context.

It is not a portable request type and no application implements it. A shell wraps its own transport in one of these, and the wrapper is a few lines because both transports already have every one of these values — what they disagree about is only how to spell them.

type Registry

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

Registry holds the components a deployment publishes for redraw.

Nothing is registered implicitly. Being exported, single-rooted, and renderable is not enough, because publishing an endpoint must be deliberate.

func (*Registry) MustRegister

func (reg *Registry) MustRegister(component Reloadable)

MustRegister is Register for a caller with nowhere to return an error, such as a package-level registry value.

func (*Registry) Register

func (reg *Registry) Register(component Reloadable) error

Register adds a component to the redraw surface.

A repeated kind is refused rather than overwritten. The kind covers a component's name, parameters, and compiled markup but not its package, so two identical templates in different packages produce the same one; silently keeping the last registration would then serve a component that looks the same but calls its own package's external functions.

This must fail at startup, and failing at startup is not the same as panicking: a caller running its own startup validation pass collects what is wrong and reports all of it, rather than aborting the process on the first one. So the failure is returned, and a caller that wants the abort still has it one line away.

func (*Registry) RequiredAssets

func (reg *Registry) RequiredAssets() []htmlbind.Asset

RequiredAssets is every registered component's required files, deduplicated by identity and in registration order. It is RequiredHead read as identity rather than as markup, for a caller deciding where each file is served.

func (*Registry) RequiredHead

func (reg *Registry) RequiredHead() []string

RequiredHead is every registered component's head contribution, deduplicated and in registration order.

It is what a caller puts in its document shell. A redraw addresses a region on a page this endpoint did not render, so it can only swap markup into a head somebody else already wrote; a component whose stylesheet is not there renders unstyled, and nothing about the swap can repair that afterwards.

Reading it needs no request and no render, so a document shell built once at startup covers every redraw the deployment will ever serve.

type Reloadable

type Reloadable struct {
	// KindID is the generated component identity, name plus a hash of its
	// parameters and compiled plan. Editing the template changes it, so a page
	// loaded before a deploy requests a kind that no longer exists.
	KindID string
	// Render decodes the query values and returns the bound component. It is
	// generated code: the decoder is typed, and an unknown name or an
	// undecodable value is an error rather than a zero value.
	//
	// The first parameter is the request's context rather than the request.
	// Generated code reads neither, and a component that authorizes its own
	// inputs needs the caller's session identity, which reaches it through the
	// context its middleware already populated — not through the transport.
	// Taking the narrower value is also what lets one registration serve a
	// backend whose request type is not *http.Request.
	Render func(ctx context.Context, instanceID string, values url.Values) (htmlbind.Fragment, error)
	// Head is what this component contributes to a document head: the merged,
	// ready-to-write tags of the component and everything it calls.
	//
	// A redraw rewrites a region of a page this endpoint never rendered, so
	// unlike a navigation it cannot merge into a head it owns. Publishing the
	// contribution is what lets a caller put it in the document shell before
	// anything is swapped, which is the only way nothing is fetched mid-swap.
	Head []string
	// Assets names the static files this component requires. It is Head read as
	// identity rather than as markup, which is what a caller needs to decide
	// whether the page already carries them.
	Assets []htmlbind.Asset
}

Reloadable is one component published as a redraw endpoint.

Registering a component publishes an HTTP endpoint whose parameters anyone can supply, so the component authorizes its own inputs exactly as an ordinary handler does. Registration is the review point: a component that only formats values handed to it is safe, while one that loads a record by identifier must check ownership itself.

type Response

type Response struct {
	// Status is what this answer means. A caller may serve another, and an
	// update response is applied by a client whatever the status says, which is
	// how a rejected form returns 422 and still rewrites its own region.
	Status int
	// Header is what the response has to carry: the Vary axes it depends on, its
	// content type, the served mode echoed back, and an entity tag where the
	// answer has one. It carries no cache policy, which is the caller's.
	Header http.Header
	// Body is the bytes to write.
	Body []byte
	// Failure is why this answer is a refusal, or nil for an ordinary one.
	//
	// It travels here rather than through a hook that writes: a caller that only
	// wants to observe logs it and sends the response as it stands, and one that
	// wants its own error page reads the kind and writes that instead. Either way
	// nothing is written until the caller writes it, which is the whole of this
	// package's position on responses.
	Failure *Failure
}

Response is a complete answer this package computed and did not send.

It exists for the entries whose headers depend on the body — a redraw digests what it rendered — because such an entry cannot both write the body and leave the headers alone. The buffered entries return one; the streaming entries take their headers from Headers instead, since a stream commits before its first record.

func FailureResponse

func FailureResponse(failure Failure) Response

FailureResponse is the refusal this package computes, exported so a caller raising one of its own — a redraw it declined before this package saw it — answers in the same shape rather than reimplementing five status codes.

The body is RFC 9457 problem details, which is this module's documented error format everywhere else; the update endpoints were the only paths writing plain text. The media type is what tells the two apart on the wire: application/json is an update to apply, including a non-2xx one, and application/problem+json is a request that produced no update at all.

The status still directs. A client's rule — any non-2xx falls back to an ordinary navigation — is unchanged, so a client that cannot read the body still lands correctly; the body adds diagnosis rather than direction.

Nothing is sent until a caller sends it: [Response.WriteTo] does that, and a caller with its own error page sends that instead.

It carries no Vary, having no Options and no request to compute one from. The refusals this package produces get theirs added; a caller raising one of its own adds them from Options.RedrawHeaders or Options.Headers, and a cacheable status makes that matter — see the note on Options.failure.

func (Response) NotModified

func (resp Response) NotModified(r Reader) bool

NotModified reports whether the request already holds this answer, by comparing its If-None-Match against the entity tag this response carries.

Answering it is the caller's: a 304 is a cache policy decision, and this package no longer makes one. A response with no entity tag is never a match.

type RuntimeConfig

type RuntimeConfig struct {
	// Build is the identity of the binary that rendered the page.
	Build string `json:"build"`
	// Attr is the data-attribute prefix, which names the instance attribute the
	// runtime locates by and the preserve and ignore markers authors write.
	Attr string `json:"attr"`
	// Header is the header namespace the render, manifest, and build headers
	// are derived from.
	Header string `json:"header"`
	// Global is the name the runtime instance is installed under. Empty
	// installs nothing, which is what a caller using only the factory wants.
	Global string `json:"global"`
	// CSRFHeader is the header the runtime puts the token in. It is not derived
	// from Header, because X-CSRF-Token is a name every framework already
	// recognizes rather than one this module owns.
	CSRFHeader string `json:"csrfHeader,omitempty"`
	// CSRF is the session's token. It is empty for a deployment that turned the
	// token off, and then the runtime sends no header, so a page without one is
	// byte-identical to what it was before this existed.
	//
	// A token in a data attribute is readable by script, which is the same
	// exposure the hidden field in every form already has. It is not what
	// protects against XSS: script that runs in the page can act as the user
	// whether or not it can read this.
	CSRF string `json:"csrf,omitempty"`
}

RuntimeConfig is what the browser runtime reads to learn its own names.

It is exported because a framework merging the runtime into its own asset builds the same object and passes it to the factory directly, rather than reproducing the field names from the JavaScript.

type StreamPlan

type StreamPlan struct {
	// Mode is what was negotiated. ModeDocument means this request gets the
	// complete document and no stream is opened at all.
	Mode Mode
	// Version is the wire version the request claimed, echoed in the terminator.
	Version int
	// Head is the merged head of the composition, written as the first record so
	// a stylesheet lands before the markup that needs it.
	Head []string
	// Live says this stream holds subscriptions open, which decides whether the
	// terminator can mean "come back".
	Live bool
	// Sequences says the client can walk a sequence tree, so a fragment may
	// travel as an address and its values instead of as markup.
	Sequences bool
	// ExpectLive marks a navigation whose composition owns a live boundary, so
	// the client knows a live request is worth issuing.
	ExpectLive bool
	// Known is what the client already holds.
	Known delta.Manifest
	// Options are the render options resolved for this stream, caller's last.
	Options []htmlbind.Option
	// Buffered is the whole delta, for the entry that renders before it writes.
	// Nil for the streaming entries, which render as they go.
	Buffered *delta.Delta
}

StreamPlan is everything a stream entry decides before it commits.

It exists because one transport cannot read its request after the response is open. fasthttp writes a streamed body from a callback that runs once the handler has returned, and forbids touching the request context from inside it, so everything the delivery needs has to be captured while the handler still owns the request. Planning first is what makes the same loop run under both transports rather than two loops agreeing.

func (StreamPlan) Streams

func (p StreamPlan) Streams() bool

Streams reports whether this plan opens a stream at all.

type Update

type Update struct {
	TargetID string
	Fragment htmlbind.Fragment
}

Update is one region an action response rewrites.

TargetID must match the id the rendered root element carries, because the browser locates the region by that id and the replacement has to keep it.

func Replace

func Replace(targetID string, fragment htmlbind.Fragment) Update

Replace pairs a target element id with the fragment that replaces it.

Jump to

Keyboard shortcuts

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