Documentation
¶
Overview ¶
Package delta compares two renders of one chain and expresses the difference as operations a browser applies, so a screen already showing the document reaches the server's fresh render without a full page load.
It is the update half of htmlbind. The render half exposes only the observation seam — htmlbind.Collector — and everything derived from it lives here: manifests, keyed validators, canonical input encoding, and deltas. The split is what it costs, or rather does not cost: an application that only renders documents links none of the hashing and encoding this package needs to authenticate validators.
Index ¶
- Constants
- func CanonArray[T any](values []T, encode func(T) string) string
- func CanonBool(value bool) string
- func CanonBytes(value []byte) string
- func CanonFloat(value float64) string
- func CanonInt(value int) string
- func CanonJoin(parts ...string) string
- func CanonOptional[T any](value *T, encode func(T) string) string
- func CanonRecord(fields string) string
- func CanonString[T ~string](value T) string
- func CanonTime(value time.Time) string
- func CanonURL(value url.URL) string
- func DeltaStreamHead(wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment, ...) ([]string, error)
- func RenderDeltaStream(ctx context.Context, key []byte, known Manifest, wrappers []htmlbind.Wrapper, ...) iter.Seq2[DeltaRecord, error]
- type Delta
- type DeltaRecord
- type Instance
- type Manifest
- type Operation
Constants ¶
const ( // OpReplace swaps a boundary's own markup, holes and all. OpReplace = "replace" // OpChildren says a boundary's own markup is unchanged and its nested // boundaries are now these, in this order. // // It carries no HTML. The client reconciles what it already holds against // the list: an id it holds and the list keeps stays, moving if the order // moved; an id the list drops is removed; an id it does not hold arrives as // its own operation in the same response. // // It exists because appending one row to a list is the ordinary event on a // live screen, and expressing it by replacing the parent costs the whole // list of holes — measured at 7,383 bytes to add one 76-byte row to a // hundred, where the list of ids costs a few hundred. OpChildren = "children" )
Variables ¶
This section is empty.
Functions ¶
func CanonArray ¶
CanonArray encodes a slice, delegating each element.
func CanonJoin ¶
CanonJoin concatenates the encoded parameters of one component in declaration order.
func CanonOptional ¶
CanonOptional encodes an absent value distinctly from any present one.
func CanonRecord ¶
CanonRecord wraps the already encoded fields of a declared record.
func CanonString ¶
CanonString encodes any string-kinded value, covering plain strings, decimals, generated enums, and the trusted string types.
func CanonTime ¶
CanonTime encodes an instant in UTC, so two equal instants in different zones cannot produce two validators.
func DeltaStreamHead ¶
func DeltaStreamHead(wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment, options ...htmlbind.Option) ([]string, error)
DeltaStreamHead is the merged head of a chain, which a streamed delta sends before any region so a newly reachable component's stylesheet is installed before the markup that needs it.
func RenderDeltaStream ¶
func RenderDeltaStream(ctx context.Context, key []byte, known Manifest, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment, options ...htmlbind.Option) iter.Seq2[DeltaRecord, error]
RenderDeltaStream renders the chain and yields the boundaries that changed, then each await boundary as it settles.
It is the streaming counterpart of RenderDelta, and the difference is worth stating: RenderDelta blocks until every await settles and compares finished regions, while this yields each region with its fallback in place and follows with the replacements. A slow dependency delays only its own region.
The document markup outside every boundary is discarded, because a delta reuses the browser's existing document shell.
Types ¶
type Delta ¶
type Delta struct {
// Manifest is the full state after the operations apply. Unchanged
// boundaries appear here without an operation, which is the entire point.
Manifest Manifest
// Operations are ordered outermost first, so a target always exists by the
// time a later operation addresses something inside it.
Operations []Operation
// Head is the merged head of the new composition. A delta reuses the live
// document shell, so a component appearing for the first time has no link
// tag installed; the client diffs this against the head it already has and
// waits for new stylesheets before applying content.
Head []string
}
Delta is the result of comparing a fresh render against what the browser already holds.
func RenderDelta ¶
func RenderDelta(key []byte, known Manifest, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment, options ...htmlbind.Option) (Delta, error)
RenderDelta renders the chain and returns only the boundaries whose markup differs from known.
The render always runs: a component may read anything, so equal inputs do not prove equal output. Only transmission is skipped, never execution. An empty known manifest is not an error and yields every boundary, which is how a freshly loaded page gets its first comparable state.
The document markup outside every boundary is discarded, because a delta reuses the browser's existing document shell.
type DeltaRecord ¶
type DeltaRecord struct {
// Operation names a boundary the comparison produced. Its HTML is empty for
// an unchanged boundary, which still carries its validator so the client can
// rebuild the whole manifest from what it received.
Operation *Operation
// Frame is the validator of the boundary Operation names.
Frame string
// Children digests that boundary's nested boundary ids, in order, and Parent
// names the boundary enclosing it.
//
// A manifest entry has three fields beside its id, and a client rebuilding
// one from a stream must be able to return all three. Without Children every
// list looks reordered on the request after next; without Parent a removal
// cannot be attributed to the boundary that would report the survivors, so a
// shrinking list falls back to replacing the outermost boundary — expensive
// in exactly the case the children operation exists to make cheap.
Children string
Parent string
// Completion is an await boundary that settled, addressed by the
// placeholder written during the initial pass rather than by an instance id.
Completion *htmlbind.Content
}
DeltaRecord is one item of a streamed delta: either a boundary the browser must install, or an await boundary that settled after the initial pass.
The two travel on one sequence because they are the same event to a client: a region of the page is ready. Splitting them in the protocol would mean two consumers applying markup to one document.
type Instance ¶
type Instance struct {
// ID identifies the same logical instance across two renders. It is derived
// from the chain position, so changing search parameters does not rename it.
ID string
// ParentID is the enclosing boundary, empty for the outermost one.
ParentID string
// ComponentID is the generated declaration identity and version.
ComponentID string
// InputValidator digests the canonical declared inputs. It predicts equal
// output only for a component whose output depends on nothing else, so it
// is a cache and diagnostic key rather than authority to skip a render.
InputValidator string
// FrameValidator digests this boundary's own rendered bytes, excluding the
// output of nested boundaries and the holes where they sit. A layout whose
// frame is unchanged can keep its DOM while its child is replaced.
FrameValidator string
// ChildrenValidator digests the ids of the nested boundaries, in order.
//
// It is separate from the frame because the two have different remedies. A
// changed frame means the component's own markup moved, and the parent is
// replaced. A changed children list means a region was inserted, removed,
// or reordered, and the parent's own DOM is fine — the client is told the
// new order and reconciles what it already holds. Folding the second into
// the first would make every appended list row cost the whole list.
//
// It is empty for a boundary with no nested boundary, which is most of them.
ChildrenValidator string
}
Instance is one update boundary as it appeared in a render.
type Manifest ¶
type Manifest struct {
Instances []Instance
}
Manifest is the update state of one render, in document order.
func CollectChain ¶
func CollectChain(w io.Writer, key []byte, wrappers []htmlbind.Wrapper, leaf htmlbind.Fragment, options ...htmlbind.Option) (Manifest, error)
CollectChain renders like htmlbind.RenderChain and additionally returns the update manifest of the boundaries it wrote. Every chain member declaring a boundary becomes one instance, so the manifest describes the layout chain rather than every component call.
key authenticates the returned validators. A digest published to a browser must be keyed, because an unkeyed hash of low entropy content lets anyone confirm a guess by comparing digests. The same key must be used for two renders that are to be compared; changing it forces a full render, which is the intended effect of a key rotation.
Collecting emits the instance attribute on each boundary's root element, so its output differs from RenderChain by exactly those attributes.
type Operation ¶
type Operation struct {
// Kind is replace. Insert, remove, and move arrive with structural
// boundaries; until then a structural change is expressed by replacing an
// enclosing boundary.
Kind string
// InstanceID names the boundary the operation targets.
InstanceID string
// HTML is the boundary's own markup, including its root element, with an
// inert placeholder where each nested boundary sits rather than that
// boundary's bytes.
HTML string
// Sequence addresses this fragment's static half, and Values are the varying
// half a client walks it with. Together they reproduce HTML exactly, which
// is what lets a caller send one or the other: the statics travel once per
// client and the values travel per render.
//
// Both are empty for a boundary whose sequence could not be derived, which
// is the case a caller falls back to HTML for.
Sequence string
Values []string
// Boundaries names the nested boundaries appearing as holes in HTML.
//
// It is what tells a hole to fill from one to retain: an id also carrying an
// operation in this response is replaced, and one that does not is a
// boundary the client already holds and moves its live node into. Nothing in
// the markup distinguishes the two, and without the list a missing fragment
// would be indistinguishable from a truncated response.
Boundaries []string
}
Operation is one change the browser applies to reach the server's render.