quicken

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jul 10, 2026 License: MIT Imports: 19 Imported by: 0

README

quicken

Fast-shell, deferred-region rendering for Go web apps. Paint the shell now, fill the expensive parts as they become ready, stay readable without JavaScript.

Status: phase 4. Deferred first render over a streaming HTML transport, a ClientFetch transport with prefetch-on-intent, and a LiveChannel transport for server-held live regions. The markup marker document and html/template helper authoring adapters have landed; explorer integration remains for a later phase.

License: MIT (Copyright (c) 2026 Goforge).

Install

Requires Go 1.26 or newer.

go get github.com/brain-fuel/quicken

The library imports only the standard library; it pulls in no third-party runtime dependencies.

Usage

page := quicken.NewPage(func(f *quicken.Frame) template.HTML {
    return template.HTML("<!doctype html><html><head>" + string(f.Head()) +
        "</head><body>" + string(f.Slot("cards")) + "</body></html>")
}).Add(quicken.RegionFunc("cards",
    func(quicken.RenderContext) quicken.Tree { return quicken.Text("<p>loading</p>") },
    func(quicken.RenderContext) quicken.Tree { return quicken.Text(expensiveHTML()) },
))

mux := http.NewServeMux()
quicken.Mount(mux)                 // serves the swap shim
mux.Handle("/", page.Handler(nil)) // nil transport uses StreamHTML

With JavaScript the region is relocated into its slot as it finishes; without JavaScript its content stays readable at the end of the document.

ClientFetch and prefetch

The ClientFetch transport sends a fast shell with skeletons and no region content; the client fetches each region from /_regions/<page>/<id> after load. Mount it with Serve so those endpoints exist:

ClientFetch is a JavaScript enhancement: with scripting disabled the page shows only skeletons. Use the default StreamHTML transport when a no-JavaScript content floor is required.

mux := http.NewServeMux()
quicken.Mount(mux)
quicken.Serve(mux, "/", page.Named("demo"), quicken.ClientFetch{})

Prefetch-on-intent warms the client cache before a click. Mark a trigger element with data-q-prefetch="/_regions/demo/cards" and, optionally, data-q-prefetch-on="mouseover" (the default), focusin, or visible. The cache is shared with region fetches, so a prefetched url loads instantly.

Live regions

AddLive registers a region that keeps server-held state across events, instead of the one-shot Render(ctx) a deferred Region uses. Mount it with Serve and the LiveChannel{} transport:

page := quicken.NewPage(shell).Named("demo").
    AddLive(myCounter{})

mux := http.NewServeMux()
quicken.Mount(mux)
quicken.Serve(mux, "/", page, quicken.LiveChannel{})

myCounter implements LiveRegion: Mount produces the initial State, HandleEvent applies a named client event to that state, and Render(State) produces the region's Tree. The page's shell marks up an event source with data-live-click="<event name>" (or another data-live-* binding) inside the region's slot; a click on it sends the named event to the server, which applies it, diffs the result against what the client already has, and pushes back only the dynamic slots that changed.

The client opens a WebSocket to the server and carries a resume token (minted on first load and embedded in the page) so a reconnect reattaches to the same session's state rather than remounting. When a WebSocket cannot be opened, or one drops, the client falls back to an HTTP long-poll transport automatically, using the same token and message shapes.

LiveChannel is a JavaScript enhancement: with scripting off, a live region shows its skeleton and nothing more, since there is no socket to carry state or events. When a no-JavaScript content floor is required, use the default StreamHTML transport instead.

Production notes

Two limitations apply to LiveChannel in this release:

  • The built-in in-memory session store never evicts a session, so every page load adds a LiveSession that lives for the process lifetime. This is fine for development and small deployments, but for production supply a bounded SessionStore (the interface is the seam for a TTL or LRU store). A tracked follow-up will add an evicting default.
  • The WebSocket upgrade does not check the Origin header. An application that needs cross-site request protection should validate Origin itself before upgrading. Note that driving a live region already requires the unguessable per-session resume token embedded in the page, so an event cannot be injected without first reading that page.

Authoring

A page's shell can be authored three ways, and all three produce an ordinary *Page, so any transport (StreamHTML, ClientFetch, LiveChannel) serves them identically.

  • Func-registry. Write the shell as a Go function over a *Frame, calling f.Head() and f.Slot(id) where the shim and each region belong. This is the style shown in Usage above, and it gives full control since the shell is plain Go.

  • Marker document. Write the shell as an HTML string carrying quicken markers, and hand it to FromMarkup. <!--quicken head--> marks the shim, and <!--quicken lazy id--> or <!--quicken live id--> marks a region's slot. Register the regions with Add and AddLive as usual; the slot each marker produces follows the registration.

    page := quicken.FromMarkup(`<!doctype html><html>
    <head><!--quicken head--></head>
    <body><!--quicken lazy cards--></body></html>`).
        Add(cardsRegion)
    
  • Template helpers. Author the shell as an html/template, using the lazy, live, and quickenHead funcs from Helpers() to emit the same markers a marker document would. Render the template with RenderMarkup, then hand the result to FromMarkup.

    tmpl := template.Must(template.New("page").Funcs(quicken.Helpers()).Parse(page))
    markup, _ := quicken.RenderMarkup(tmpl, data)
    page := quicken.FromMarkup(markup).Add(cardsRegion)
    

    A literal <!--quicken ...--> comment hand-typed directly into an html/template source is stripped by html/template and produces nothing, so an author working in html/template must use the Helpers() funcs (which return template.HTML) rather than typing the marker comment by hand; a marker document passed straight to FromMarkup (not routed through html/template) is unaffected by this and can contain the literal comment.

Because the marker document and template-helper styles both go through FromMarkup, and FromMarkup builds an ordinary func-registry *Page underneath, all three styles are interchangeable: switching how a shell is authored never changes what gets delivered.

Testing

The library code is standard-library only; chromedp is a test-time dependency (imported only by the browser test), so consumers of the library never build it. The headless-browser end-to-end test is part of the normal suite and is default-skipped. Run it opt-in:

QUICKEN_BROWSER_TEST=1 go test ./...

It skips cleanly when no browser is installed.

Documentation

Overview

Package quicken renders web pages as a fast shell plus deferred regions.

A page paints its shell and lightweight skeletons immediately, then fills each region with its real content as that content becomes ready. The default transport streams over one HTTP response and stays readable with JavaScript disabled. A ClientFetch transport fetches each region after load, and a LiveChannel transport keeps a region live over a WebSocket (falling back to HTTP long-poll), pushing fine-grained patches as its server-held state changes.

A page's shell can be authored as a Go function over a Frame (the func-registry style), as an HTML marker document parsed by FromMarkup, or as an html/template rendered with the funcs from Helpers and then passed to FromMarkup. All three styles produce the same Page, so a transport serves them identically regardless of how the shell was written.

Two limitations apply to the LiveChannel transport in this release. The built-in session store keeps every session in memory and never evicts one, so each page load adds a session that lives for the process lifetime; supply a bounded SessionStore for production use. The WebSocket upgrade performs no same-origin (Origin header) check, so an application that needs cross-site request protection should validate the Origin itself. Driving a live region still requires the unguessable per-session resume token embedded in the page.

Index

Constants

View Source
const ScriptPath = "/_quicken/quicken.js"

ScriptPath is where the shim is served and referenced from the shell head.

Variables

View Source
var (
	Text       = cadence.Text
	Slots      = cadence.Slots
	RegionFunc = cadence.RegionFunc
)

Functions

func Escape

func Escape(s string) string

Escape returns s with HTML metacharacters escaped, for interpolating untrusted data into a dynamic Tree slot. Tree emits slot content raw (regions own their markup), so wrap any user-derived value with Escape.

func Helpers added in v0.4.0

func Helpers() template.FuncMap

Helpers returns template funcs for authoring a quicken page as an html/template: lazy and live emit a region marker for the given id, and quickenHead emits the shim marker. Render the template with RenderMarkup and pass the result to FromMarkup. lazy and live panic on an invalid id, which is always a programming error.

func Mount

func Mount(mux *http.ServeMux)

Mount registers the shim route on mux at ScriptPath.

func RenderMarkup added in v0.4.0

func RenderMarkup(t *template.Template, data any) (string, error)

RenderMarkup executes a template into a string, ready for FromMarkup.

func ScriptHandler

func ScriptHandler() http.Handler

ScriptHandler serves the embedded swap shim as JavaScript.

func Serve

func Serve(mux *http.ServeMux, path string, p *Page, t Transport)

Serve mounts a page and its transport's routes on mux: the page handler at path, plus any routes the transport provides via RouteProvider. Call Mount once, separately, to serve the client shim. A nil transport uses StreamHTML.

Types

type ClientFetch

type ClientFetch struct{}

ClientFetch delivers the shell with skeletons immediately and lets the client fetch each region from its own endpoint after load. The initial response contains no region content, so it is small and fast; regions fill in via the shim's fetch path and can be prefetched on intent. Use Serve so the per-region endpoints (Routes) are mounted alongside the page.

func (ClientFetch) Deliver

func (ClientFetch) Deliver(w http.ResponseWriter, r *http.Request, p *Page) error

Deliver implements Transport. It writes the shell with skeletons plus a JSON manifest telling the client which regions to fetch and from where. It does not render any region.

func (ClientFetch) Routes

func (ClientFetch) Routes(p *Page) map[string]http.Handler

Routes implements RouteProvider: one endpoint per region that renders just that region, plus a 404 guard at the region prefix. The exact per-region routes are more specific than the guard, so known ids resolve and unknown ids return Not Found rather than falling through to a catch-all page handler.

type Frame

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

Frame is handed to a Shell so it can place region skeletons and the shim.

func (*Frame) Head

func (f *Frame) Head() template.HTML

Head returns the markup for the document head: the shim script tag.

func (*Frame) Slot

func (f *Frame) Slot(id string) template.HTML

Slot returns the placeholder for the region with the given id: a slot element carrying the region's skeleton, which the transport later fills.

type LiveChannel

type LiveChannel struct {
	Store SessionStore
	// contains filtered or unexported fields
}

LiveChannel is the live transport: a WebSocket carries the first render and every subsequent fine-grained patch. A zero value uses a process-wide in-memory session store; set Store to supply your own.

pollTimeout controls how long the long-poll GET endpoint blocks waiting for the next queued message before responding 204. Zero means a default (see defaultPollTimeout in longpoll.go).

func (LiveChannel) Deliver

func (lc LiveChannel) Deliver(w http.ResponseWriter, r *http.Request, p *Page) error

Deliver renders the shell with skeletons and a live manifest, mints a session, and mounts every live region so its state is ready when the socket connects. It does not stream any region's live HTML: the client renders "first" messages received over the socket, and the skeleton already in the document is the JS-off floor.

func (LiveChannel) Routes

func (lc LiveChannel) Routes(p *Page) map[string]http.Handler

Routes mounts the WebSocket endpoint and the long-poll fallback endpoints for this page.

type LiveRegion

type LiveRegion = cadence.LiveRegion

The region model and content substrate live in cadence, the foundation module. quicken re-exports them under its own names so its transports, pages, and authoring adapters (and their tests) keep working unchanged while depending on the shared foundation.

type LiveSession

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

LiveSession holds one connection's per-region state, keyed by region id.

type Page

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

Page is a lazy page: a shell plus its registered regions.

func FromMarkup added in v0.4.0

func FromMarkup(markup string) *Page

FromMarkup builds a Page from an HTML document carrying quicken markers: <!--quicken head--> for the client shim, and <!--quicken lazy id--> or <!--quicken live id--> for a region's slot. Register the regions with Add and AddLive; the slot each marker produces follows the registration. It panics on malformed markup or a duplicate region or head marker, which is always a programming error.

The markup is expected to be author-controlled or trusted: a compile-time constant or the output of a trusted template, analogous to html/template.Must. An application assembling markup from dynamic or untrusted input should validate it first or recover from the panic.

func NewPage

func NewPage(shell Shell) *Page

NewPage creates a page with the given shell.

func (*Page) Add

func (p *Page) Add(r Region) *Page

Add registers a region. Regions render in the order added. Adding a region whose id is a duplicate or is not of the form [A-Za-z0-9_-]+ panics, which is always a programming error.

func (*Page) AddLive

func (p *Page) AddLive(r LiveRegion) *Page

AddLive registers a live region. Live regions render in the order added. A duplicate id (in either the deferred or live set) or an id not of the form [A-Za-z0-9_-]+ panics, which is always a programming error.

func (*Page) Handler

func (p *Page) Handler(t Transport) http.Handler

Handler returns an http.Handler that serves the page with the given transport. A nil transport uses StreamHTML.

func (*Page) Named

func (p *Page) Named(name string) *Page

Named sets the page's name, used to namespace its per-region fetch endpoints (see ClientFetch). The name must be empty or of the form [A-Za-z0-9_-]+; an invalid name panics. Returns the page for chaining.

type Params

type Params = cadence.Params

The region model and content substrate live in cadence, the foundation module. quicken re-exports them under its own names so its transports, pages, and authoring adapters (and their tests) keep working unchanged while depending on the shared foundation.

type Payload

type Payload = cadence.Payload

The region model and content substrate live in cadence, the foundation module. quicken re-exports them under its own names so its transports, pages, and authoring adapters (and their tests) keep working unchanged while depending on the shared foundation.

type Region

type Region = cadence.Region

The region model and content substrate live in cadence, the foundation module. quicken re-exports them under its own names so its transports, pages, and authoring adapters (and their tests) keep working unchanged while depending on the shared foundation.

type RenderContext

type RenderContext = cadence.RenderContext

The region model and content substrate live in cadence, the foundation module. quicken re-exports them under its own names so its transports, pages, and authoring adapters (and their tests) keep working unchanged while depending on the shared foundation.

type RouteProvider

type RouteProvider interface {
	Routes(p *Page) map[string]http.Handler
}

RouteProvider is an optional Transport capability: extra routes a transport needs mounted alongside the page, such as the per-region fetch endpoints of ClientFetch. Serve mounts these automatically.

type SessionStore

type SessionStore interface {
	Get(token string) (*LiveSession, bool)
	Put(token string, s *LiveSession)
	Delete(token string)
}

SessionStore maps a resume token to a LiveSession. The in-memory implementation is the only one built for v1; the interface lets a shared store replace it later without touching the core.

func NewMemoryStore

func NewMemoryStore() SessionStore

NewMemoryStore returns an in-process SessionStore.

type Shell

type Shell func(f *Frame) template.HTML

Shell renders the full HTML document for a page. It places each region's skeleton with f.Slot(id) and includes the shim with f.Head(). It returns a complete document; the transport streams region fills in just before the closing body tag.

type State

type State = cadence.State

The region model and content substrate live in cadence, the foundation module. quicken re-exports them under its own names so its transports, pages, and authoring adapters (and their tests) keep working unchanged while depending on the shared foundation.

type StreamHTML

type StreamHTML struct{}

StreamHTML flushes the shell with skeletons immediately, renders regions concurrently, and streams each region's real HTML as a fill block with an inline swap script as it becomes ready. With scripting disabled the fill blocks stay visible after the body content, so the page is still fully readable; the shim only relocates them into their slots.

func (StreamHTML) Deliver

func (StreamHTML) Deliver(w http.ResponseWriter, r *http.Request, p *Page) error

Deliver implements Transport.

type Transport

type Transport interface {
	Deliver(w http.ResponseWriter, r *http.Request, p *Page) error
}

Transport delivers a page's shell and regions to the client. StreamHTML is the default; ClientFetch and later live transports sit behind this same interface.

type Tree

type Tree = cadence.Tree

The region model and content substrate live in cadence, the foundation module. quicken re-exports them under its own names so its transports, pages, and authoring adapters (and their tests) keep working unchanged while depending on the shared foundation.

Jump to

Keyboard shortcuts

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