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
- Variables
- func DecodeManifest(encoded string) htmlbind.Manifest
- func EncodeManifest(manifest htmlbind.Manifest) string
- func QueryBool(values url.Values, name string, target *bool) error
- func QueryFloat(values url.Values, name string, target *float64) error
- func QueryInt(values url.Values, name string, target *int) error
- func QueryOptional[T any](values url.Values, name string, target **T) error
- func QueryString[T ~string](values url.Values, name string, target *T) error
- func QueryTime(values url.Values, name string, target *time.Time) error
- func QueryURL(values url.Values, name string, target *url.URL) error
- func RuntimeVersion() string
- type DeltaStream
- func (s *DeltaStream) Close() error
- func (s *DeltaStream) Fail(message string)
- func (s *DeltaStream) Replace(instanceID, html, frame string)
- func (s *DeltaStream) Sent(instanceID string) bool
- func (s *DeltaStream) Settled(boundaryID string, html []byte)
- func (s *DeltaStream) Unchanged(instanceID, frame string)
- type Mode
- type Negotiated
- type Options
- func (o Options) Mount(mux *http.ServeMux, registry *Registry)
- func (o Options) Negotiate(r *http.Request) Negotiated
- func (o Options) OpenStream(w http.ResponseWriter, head []string) *DeltaStream
- func (o Options) RedrawHandler(reg *Registry) http.Handler
- func (o Options) RedrawPath(kindID, instanceID string, values url.Values) string
- func (o Options) Render(w http.ResponseWriter, r *http.Request, wrappers []htmlbind.Wrapper, ...) error
- func (o Options) RenderLiveStream(ctx context.Context, w http.ResponseWriter, r *http.Request, ...) error
- func (o Options) RenderStream(w http.ResponseWriter, r *http.Request, wrappers []htmlbind.Wrapper, ...) error
- func (o Options) RenderStreamAsync(ctx context.Context, w http.ResponseWriter, r *http.Request, ...) error
- func (o Options) RuntimeHandler() http.Handler
- func (o Options) RuntimePath() string
- func (o Options) ScriptTag() string
- func (o Options) WantsUpdate(r *http.Request) bool
- func (o Options) WriteNavigate(w http.ResponseWriter, url string) error
- func (o Options) WriteUpdate(w http.ResponseWriter, updates ...Update) error
- func (o Options) WriteUpdateStatus(w http.ResponseWriter, status int, updates ...Update) error
- type Registry
- type Reloadable
- type Update
Constants ¶
const DefaultHeaderPrefix = "X-Tinybind"
DefaultHeaderPrefix names the request and response headers.
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.
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.
const MaxQueryBytes = 4 << 10
MaxQueryBytes bounds the arguments a redraw may carry, since a GET puts every one of them in the URL.
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.
const Version = htmlbind.ProtocolVersion
Version is the wire contract this package speaks.
Variables ¶
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 ¶
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 ¶
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 QueryFloat ¶
QueryFloat decodes a float parameter.
func QueryOptional ¶
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 ¶
QueryString decodes any string-kinded parameter, covering plain strings, decimals, and generated enums.
func QueryTime ¶
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 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 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.