livepage

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 20 Imported by: 0

README

livepage

livepage is a streaming HTML UI layer for go-app components synchronized by asyncmachine-go. It allows for unified WASM and server-streamed HTML UIs. It's like datastar, but not for web pages.

Example component:

// counter has a value and a button increasing the value.
type counter struct {
  livepage.Compo

  mach *am.Machine
  n    int
}

func (c *counter) Mach() *am.Machine { return c.mach }

func (c *counter) Render() UI {
  return Div().Body(
    Span().Text(c.n),
    Button().Text("+").
      OnClick(func(app Context, e Event) {
        c.n += 1
      }),
  )
}

Page initialization:

import (
  am "github.com/pancsta/asyncmachine-go/pkg/machine"
  "github.com/pancsta/livepage"
)

// ...
  
mach := am.New(ctx, am.Schema{am.StateReady: {}}, nil)
page, err := livepage.New(ctx, &counter{mach: mach}, livepage.Opts{
  Debounce: 10 * time.Millisecond,
  DiffHtml: true,
})
srv := httptest.NewServer(page.NewHandler())

How It Works

livepage does NOT have:

  • URLs or links
  • API endpoints or REST
  • thick JS client
  • magic attrs like data-on:my-event="$foo = evt.detail"
  • JSON
  • markup pre-processing / generation step

livepage DOES have:

  • Golang only
  • thin JS client
  • DOM diff updates from idiomorph
  • network diffs from go-diff
  • inline DOM handlers from go-app

The rendering happens on the event loop provided by asyncmachine-go and does not require locking. Multiple browsers connecting to a single livepage will see the same mirrored HTML projection with realtime updates. Plain and simple.

Why?

The Web is made to optimize the load on the server. This requirement goes away when developing desktop / active / live / persistent / single-user apps. Instead of streaming raster data, this UI layer streams HTML. It can also be used to gain access to the Delve debugger for go-app WASM apps which can now be fully deployed on the server using a regular compilation target, while still rendering the UI in the browser.

Status

This is an early version extracted from a prototype. Both the prototype and this version have been shipped to production. Further work is expected, especially for deeper state machine integration via schemas and to support additional transports (CDP, aRPC, IPC). ATM it depends on a thin fork of go-app.

Documentation

Overview

Package livepage serves a go-app component as a live HTML page. The browser connects over a WebSocket (github.com/coder/websocket), forwards DOM events (click, change, submit) to the model, and receives re-rendered HTML, which is morphed into the document using Idiomorph. jQuery and Idiomorph are served by the package and injected into the <head> of every page.

Usage:

type Model struct {
	livepage.ModelBase
	mach *am.Machine
}
// implement Render and Mach; use go-app event handlers like OnClick

page, err := livepage.New(ctx, model, livepage.Opts{})
http.ListenAndServe(":8080", page.Handler())

State changes outside of DOM events should call Page.HRefresh.

Enable Opts.DiffHtml to push compact HTML diffs instead of full documents.

Index

Constants

View Source
const (
	// DefaultDebounce is the default delay of pushing HTML to the clients.
	DefaultDebounce = 200 * time.Millisecond
	// PathWs is the WebSocket endpoint, relative to [Opts.Prefix].
	PathWs = "/ws"
	// PathAssets is the directory with served scripts, relative to
	// [Opts.Prefix].
	PathAssets = "/assets/"
)

Variables

This section is empty.

Functions

func DefaultHtmlPage

func DefaultHtmlPage(body string) string

DefaultHtmlPage is a minimal HTML document.

func ToRef

func ToRef[T h.UI](target *T, elem T) T

ToRef dumps a reference for a node within a DOM chain for further processing inside deferred funcs. It prevents the chain from being broken up.

Types

type Compo

type Compo struct {
	h.Compo
	// contains filtered or unexported fields
}

func (*Compo) App

func (c *Compo) App() *h.Context

App returns the component's app context, if mounted.

func (*Compo) OnMount

func (c *Compo) OnMount(ctx h.Context)

OnMount stores the component's context.

func (*Compo) PageName

func (c *Compo) PageName() string

func (*Compo) Update

func (c *Compo) Update()

Update flags the component for re-render. No-op until mounted.

type CompoLiveApi

type CompoLiveApi interface {
	h.Composer

	// Mach is the machine used to serialize DOM handlers and rendering.
	Mach() *am.Machine
	// Update flags the component for re-render.
	Update()
	PageName() string
}

CompoLiveApi is a live component. It has to embed Compo (or h.Compo and implement Update on its own).

type DomEvent

type DomEvent struct {
	// Type is one of click, change, submit.
	Type string

	// attrs
	Id    string
	Name  string
	Value string
	// data-livepage attr
	Data string
	// Handler is the ID of a go-app event handler bound to the target.
	Handler string

	// event payload
	Key        string  `json:",omitempty"`
	Code       string  `json:",omitempty"`
	Button     int     `json:",omitempty"`
	ClientX    float64 `json:",omitempty"`
	ClientY    float64 `json:",omitempty"`
	DeltaX     float64 `json:",omitempty"`
	DeltaY     float64 `json:",omitempty"`
	ScrollTop  float64 `json:",omitempty"`
	ScrollLeft float64 `json:",omitempty"`

	// meta
	Path string

	// internal
	Event *am.Event `json:"-"`
}

DomEvent is a DOM event forwarded from the browser.

type Opts

type Opts struct {
	// HtmlPage wraps the rendered component into a whole HTML document. Scripts
	// are injected into its <head>. Defaults to [DefaultHtmlPage].
	HtmlPage func(body string) string
	// Debounce delay of pushing HTML to the clients. Defaults to
	// [DefaultDebounce].
	Debounce time.Duration
	// DiffHtml sends HTML diffs instead of full documents, using go-diff on
	// the server and a JS equivalent in the browser. Default false.
	DiffHtml bool
	// Prefix of all the paths served by [Page.NewHandler], eg "/live". Optional.
	Prefix string
	// OriginPatterns are additionally allowed WebSocket origins (host
	// patterns). Same origin is always allowed.
	OriginPatterns []string
	// Logger defaults to slog.Default().
	Logger *slog.Logger
}

Opts configures a Page.

type Page

type Page[M CompoLiveApi] struct {
	Disposed atomic.Bool
	// contains filtered or unexported fields
}

Page binds a CompoLiveApi to many browsers.

func New

func New[M CompoLiveApi](ctx context.Context, model M, opts Opts) (*Page[M], error)

New mounts the model and creates a Page, which lives as long as ctx.

func (*Page[M]) Clients

func (p *Page[M]) Clients() int

Clients returns the number of connected browsers.

func (*Page[M]) Compo

func (p *Page[M]) Compo() M

Compo returns the underlying component.

func (*Page[M]) Dispose

func (p *Page[M]) Dispose()

func (*Page[M]) HRefresh

func (p *Page[M]) HRefresh()

HRefresh re-renders the model and pushes the HTML. It is safe to call from state handlers and Eval().

func (*Page[M]) HTML

func (p *Page[M]) HTML() (string, bool)

HTML returns the full HTML document with the current state, as sent to the browsers. It returns false when rendering timed out.

func (*Page[M]) NewHandler

func (p *Page[M]) NewHandler() http.Handler

NewHandler serves the page, scripts and the WebSocket endpoint.

func (*Page[M]) Push

func (p *Page[M]) Push()

Push schedules (debounced) pushing of the current HTML to all the clients. Nothing is sent when the HTML didn't change.

func (*Page[M]) Refresh

func (p *Page[M]) Refresh(ctx context.Context)

Refresh is threadsafe version of HRefresh and should not be called from state handlers or eval.

Jump to

Keyboard shortcuts

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