htmlupdate

package
v0.3.0 Latest Latest
Warning

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

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

Documentation

Overview

Package htmlupdate serves HTML templates that can update themselves in place.

One URL answers two ways. Without the render header a request gets the ordinary complete document, so a browser without the runtime, a crawler, and curl are all unaffected. With it, the response carries only the boundaries whose markup actually changed.

The transport concerns live here rather than in htmlbind, because that package stays free of net/http so generated template code keeps working on TinyGo and WebAssembly targets.

Index

Constants

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 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 MaxQueryBytes = 4 << 10

MaxQueryBytes bounds the arguments a redraw may carry, since a GET puts every one of them in the URL.

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

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

Version is the wire contract this package speaks.

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.

Functions

func DecodeManifest

func DecodeManifest(encoded string) htmlbind.Manifest

DecodeManifest reads the compact validator list a client sends back. The encoding is "id:validator" pairs separated by commas, which stays inside one header and needs no escaping because both halves are opaque tokens.

func EncodeManifest

func EncodeManifest(manifest htmlbind.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 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 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.

Types

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

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, frame string)

Replace writes one settled boundary and the validator it produced.

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

func (s *DeltaStream) Unchanged(instanceID, frame string)

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

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
)

type Negotiated

type Negotiated struct {
	Mode Mode
	// Version is the protocol version the client claims. It equals Version
	// whenever Mode is not ModeDocument.
	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 htmlbind.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. A deployment overriding it needs a browser runtime
	// built for the same prefix, because the runtime hardcodes the names.
	HeaderPrefix 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
}

Options configure one set of update endpoints.

func (Options) Mount

func (o Options) Mount(mux *http.ServeMux, registry *Registry)

Mount registers every framework-owned endpoint under the configured path prefix. One call keeps the whole surface routable, cacheable, and protectable by a single rule.

func (Options) Negotiate

func (o Options) Negotiate(r *http.Request) 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, a proxy that dropped a header, and a version bump must all still produce a working page.

func (Options) OpenStream

func (o Options) OpenStream(w http.ResponseWriter, head []string) *DeltaStream

OpenStream commits the response and writes the head record.

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

func (o Options) RedrawHandler(reg *Registry) http.Handler

RedrawHandler serves the registered components.

The path is <prefix>/redraw/<kind>/<instance>. The instance id travels so the returned root element arrives already addressable; the render itself depends only on the kind and the query values.

func (Options) RedrawPath

func (o Options) RedrawPath(kindID, instanceID string, values url.Values) string

RedrawPath is the URL for one instance of a registered component, exposed so a test and a non-browser client can build exactly what the runtime does.

func (Options) Render

func (o Options) Render(w http.ResponseWriter, r *http.Request, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment) error

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

It always sets Vary, because a cache that served a delta body to a document request would hand a browser a page of JSON. The caller keeps every other response concern, as elsewhere in this module.

func (Options) RenderLiveStream

func (o Options) RenderLiveStream(ctx context.Context, w http.ResponseWriter, r *http.Request, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment, options ...htmlbind.Option) error

RenderLiveStream is RenderStreamAsync for a chain holding live sources: it keeps every subscription open and writes each delivery as it arrives.

Reconnecting after a dropped stream is the same request again. Nothing has to be resumed, because a live delivery carries the whole state of its region rather than an increment, so a missed one costs nothing and boundary ids are reproduced by position.

func (Options) RenderStream

func (o Options) RenderStream(w http.ResponseWriter, r *http.Request, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment) error

RenderStream answers a navigation with a record stream instead of one buffered body, so each region applies as soon as it is written.

Everything that could change the status is decided before the first record, because writing it commits the response. After that a failure can only be reported in band.

func (Options) RenderStreamAsync

func (o Options) RenderStreamAsync(ctx context.Context, w http.ResponseWriter, r *http.Request, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment, options ...htmlbind.Option) error

RenderStreamAsync answers a navigation with a record stream that also carries await boundaries as they settle.

Each region reaches the browser with its fallback in place and is replaced when its dependency finishes, so a slow one delays only itself. A chain with no await boundary produces exactly what RenderStream does.

func (Options) RuntimeHandler

func (o Options) RuntimeHandler() http.Handler

RuntimeHandler serves the browser runtime.

The framework ships this asset rather than generating it, because the protocol it speaks is a framework constant. Serving it here keeps the first milestone free of the static asset pipeline.

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 endpoint prefix, so one shared runtime asset works for any configured namespace without being rebuilt.

func (Options) WantsUpdate

func (o Options) WantsUpdate(r *http.Request) 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(w http.ResponseWriter, url string) 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(w http.ResponseWriter, updates ...Update) 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.

func (Options) WriteUpdateStatus

func (o Options) WriteUpdateStatus(w http.ResponseWriter, status int, updates ...Update) 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 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) Register

func (reg *Registry) Register(component Reloadable)

Register adds a component to the redraw surface.

A repeated kind panics rather than overwriting. 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. Registration happens at startup, so failing there is the cheapest place to find it.

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.
	Render func(r *http.Request, instanceID string, values url.Values) (htmlbind.Fragment, error)
}

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