liquid

package
v1.1.1 Latest Latest
Warning

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

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

Documentation

Overview

Package liquid is the Liquid runtime: a server-driven UI framework with an Angular-style component model and LiveView-style interactivity. Components live and execute on the server; the browser receives rendered HTML and a small runtime script that swaps server-computed patches into the DOM.

Component model

A Component is a plain Go type that reports a Selector (its custom element tag) and a Template (compiled .lsx markup, usually generated from a paired .lsx file by liquid build). Exported struct fields are template-visible state. All interpolation flows through html/template's contextual escaping.

Register components on an App and bind them to URLs:

app := liquid.New()
_ = app.Route("/", &HomePage{})
http.ListenAndServe(":8080", app)

Templates are parsed once, at registration; every request renders a fresh component instance. Registered components act only as prototypes — they are never shared as mutable state across requests.

Hydro and sessions

"Hydro" is Liquid's interactivity layer. An interactive render mints an opaque session token and a hydro boundary; the runtime script posts user events back to the server, which dispatches them against the live component instance and returns an Envelope — an HTML patch to swap at the boundary, or a redirect. Events for one hydro session are serialized: they are never dispatched concurrently against a single live instance. The in-memory session registry is bounded (see Limits, DefaultMaxSessions); eviction keeps unauthenticated traffic from growing it without limit.

Invariants a caller must respect

  • Component instances are per-request (or per interactive session) — never shared mutable singletons across requests. App-lifetime state belongs in a service registered with App.Provide and read through an observable such as BehaviorSubject.
  • Session tokens are opaque random strings — treat them as such; never derive meaning from their bytes.
  • Event handlers are reached through a compile-time action allowlist, not by reflecting method names off client input.
  • All template output is escaped by html/template. Do not assemble HTML by string concatenation around user data.

Reactive state

BehaviorSubject is mutex-guarded observable state that always holds a current value (BehaviorSubject.Value) and notifies subscribers on BehaviorSubject.Next. The Observable combinators — Map, Throttle, CombineLatest, Interval, and Observe — derive and consume streams. Only interactive sessions hold subscriptions, and the framework cancels them when the session ends.

Loading and routing

Load and Loader describe asynchronous data a component needs; Ctx.Fanout runs several loaders together under the request's context. Guard functions decide whether a request may activate a route (returning Allow, Deny, or Redirect) and run before the component is instantiated. Head carries the document title and meta tags for a rendered page.

Primary entry points

Configure the app with Option values such as WithLimits and WithLogger. Logging goes through log/slog with a pluggable handler.

Index

Constants

View Source
const (
	// DefaultMaxSessions is the Limits.MaxSessions default.
	DefaultMaxSessions = 1024
	// DefaultMaxComponentsPerSession is the Limits.MaxComponentsPerSession
	// default.
	DefaultMaxComponentsPerSession = 64
	// DefaultSessionIdleTimeout is the Limits.SessionIdleTimeout default.
	DefaultSessionIdleTimeout = time.Hour
	// DefaultMaxEventBytes is the Limits.MaxEventBytes default: 64 KiB.
	DefaultMaxEventBytes = 64 << 10
	// DefaultMaxStreamsPerSession is the Limits.MaxStreamsPerSession
	// default: enough for a handful of tabs, small enough that a
	// cookie-holder cannot pin goroutines without bound.
	DefaultMaxStreamsPerSession = 8
)

The registry's default caps: unauthenticated traffic must not grow it without limit, so both dimensions evict at a bound even when the app configures nothing (D20).

View Source
const DefaultAuthTTL = 24 * time.Hour

DefaultAuthTTL is how long a liquid_auth cookie stays valid after login.

Variables

This section is empty.

Functions

func Notify added in v1.1.0

func Notify(ctx context.Context, n Notification) error

Notify shows a native OS notification when the app is running under the Liquid desktop shell. When it is not (the LIQUID_NATIVE_ADDR env var is unset — a normal web deployment), Notify is a no-op and returns nil, so a component can call it unconditionally and stay portable across web and desktop.

On the desktop it posts to the shell's native bridge over loopback; the shell delivers the notification natively (ADR-0009). Notifications require a bundled app: they appear from a built .app (which carries a bundle identifier), not from a bare `go run` shell.

Types

type ActionProvider

type ActionProvider interface {
	Actions() []string
}

ActionProvider is implemented by interactive components; liquid build generates Actions from the template's event bindings (D10). The server dispatches only these — a method absent from the list does not exist as far as the event endpoint is concerned.

type App

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

App routes HTTP requests to components. Component templates are parsed once, at registration; every request renders a fresh component instance — registered instances act only as prototypes and are never shared mutable state across requests.

func New

func New(opts ...Option) *App

New creates an App, applying any options.

func (*App) LiveSessions added in v1.0.0

func (a *App) LiveSessions() int

LiveSessions returns the number of interactive sessions currently held in the in-memory registry — a gauge for scraping. The registry is bounded (D20), so this cannot grow without limit. Safe for concurrent use.

func (*App) Provide

func (a *App) Provide(svc any) error

Provide registers svc as an app-lifetime singleton available for injection into component fields tagged `inject:""` (D8). Provide services before registering the routes that need them: resolution happens at Route, and an unresolvable dependency fails registration. A service is shared across requests and must be safe for concurrent use.

func (*App) Register

func (a *App) Register(c Component) error

Register adds a component to the App's registry so parent templates can nest it by selector (D14). Routing a component registers it too; Register is for components that only ever render as children. Registering the same component type again is a no-op; a selector claimed by a different type is an error.

func (*App) Route

func (a *App) Route(path string, c Component, opts ...RouteOption) error

Route registers a component to serve GET requests at path. A ":name" segment matches any single path segment and binds its decoded value to the component field tagged `pathParam:"name"`. When several routes match a request, a literal segment beats a :param at the first position they differ; exact ties fall to registration order. The component's template is parsed immediately; a template error is reported here, at registration, never at request time. The instance passed in is a prototype: its field values seed each per-request copy, so its reference-typed fields (slices, maps, pointers, …) must be nil — a shallow copy of a live reference would be shared mutable state across requests. Per-request data belongs in lifecycle hooks, not the prototype.

func (*App) Serve added in v1.0.0

func (a *App) Serve(ctx context.Context, cfg ServeConfig) error

Serve runs app as a production HTTP server on cfg.Addr until ctx is cancelled, then shuts down gracefully. Cancel ctx to trigger shutdown; the idiomatic wiring is signal.NotifyContext for SIGINT/SIGTERM:

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := app.Serve(ctx, liquid.ServeConfig{Addr: ":8080"}); err != nil {
	logger.Error("serving", "err", err)
	os.Exit(1)
}

Serve applies production timeouts and, on shutdown, drains live SSE streams so the server does not hang on long-lived connections. It returns nil on a clean graceful shutdown, or the error that stopped it — a failed bind, a serving failure, or a shutdown that exceeded ShutdownTimeout.

func (*App) ServeHTTP

func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler. A panic anywhere in the request path (guards, OnInit, render) is recovered to the framework error page — the render is buffered, so no body bytes precede it (D18).

func (*App) Static

func (a *App) Static(dir string) error

Static serves the files under dir at /static/ through the stdlib file server (D22). The directory must exist now: a bad path is a hard error at registration, matching the router's loud-misconfiguration posture.

type BehaviorSubject

type BehaviorSubject[T any] struct {
	// contains filtered or unexported fields
}

BehaviorSubject is mutex-guarded observable state (D3): it always holds a current value, readable synchronously with Value, and notifies subscribers on every Next. App-lifetime subjects belong in services registered with Provide; request-scoped reads use Value without subscribing — only interactive sessions hold subscriptions, and the framework cancels those when the session goes (D20).

func NewBehaviorSubject

func NewBehaviorSubject[T any](initial T) *BehaviorSubject[T]

NewBehaviorSubject creates a subject holding initial as its current value.

func (*BehaviorSubject[T]) Next

func (s *BehaviorSubject[T]) Next(v T)

Next sets the current value and delivers it to every subscriber. Callbacks run synchronously on the caller's goroutine but outside the subject's lock, so a callback may itself call Next or Value; under concurrent Next calls the delivery order is unspecified — the current value, not the callback argument, is authoritative.

func (*BehaviorSubject[T]) Subscribe

func (s *BehaviorSubject[T]) Subscribe(fn func(T)) (cancel func())

Subscribe registers fn for every future emission and returns its cancel. The current value is not replayed — read it with Value. Cancel is idempotent and safe to call concurrently with Next; a delivery already in flight may still land after cancel returns.

func (*BehaviorSubject[T]) Value

func (s *BehaviorSubject[T]) Value() T

Value returns the subject's current value.

type Component

type Component interface {
	// Selector returns the component's custom element tag, e.g. "app-hello".
	Selector() string
	// Template returns the component's compiled .lsx markup.
	Template() string
}

Component is a server-side UI component. Exported struct fields are template-visible state; the template is .lsx markup, usually generated from a paired .lsx file by liquid build.

type Ctx

type Ctx struct {
	context.Context
	// contains filtered or unexported fields
}

Ctx is the per-request context handed to guards and lifecycle hooks. It embeds the request's context.Context (D18), so cancellation and deadlines flow into any work a component fans out, and exposes request accessors.

func NewCtx

func NewCtx(req *http.Request, params map[string]string) Ctx

NewCtx assembles a Ctx from a request and bound route params. The framework builds its own Ctx per request; this constructor exists so test harnesses (liquidtest) can hand one to lifecycle hooks directly. req must be non-nil — liquidtest.Ctx supplies a default request for callers that have none.

func (Ctx) Fanout

func (c Ctx) Fanout(loaders ...Loader) error

Fanout runs the loaders concurrently and waits for all of them to finish, returning the first error any of them produced. It keeps waiting even after that first error (or a WithTimeout deadline): each loader holds a pointer into the component, so returning while one still runs would let a late write race the render. A failure cancels the sibling loaders' contexts, but a loader that ignores its context delays Fanout's return — it is never killed. If a loader panics, the panic is re-raised here on the calling goroutine (taking precedence over any loader error) so the router's usual recovery applies.

func (Ctx) Header

func (c Ctx) Header(name string) string

Header returns the named request header.

func (Ctx) Login added in v1.0.0

func (c Ctx) Login(principal string) error

Login attaches principal to the session: it sets a signed identity cookie and rotates the session id (fixation defense, D15), voiding pre-login CSRF tokens. It must be called from an event handler — the login flow is a form submit; calling it from a render/OnInit or background load returns an error (ADR-0007).

func (Ctx) Logout added in v1.0.0

func (c Ctx) Logout() error

Logout clears the identity cookie and rotates the session. Event-handler only, like Login.

func (Ctx) Param

func (c Ctx) Param(name string) string

Param returns the value bound to the named :param route segment, or "" when the route has no such segment.

func (Ctx) Principal added in v1.0.0

func (c Ctx) Principal() (string, bool)

Principal returns the verified identity attached to the request's session (#108, ADR-0007), or ("", false) for an anonymous request. Available in guards, OnInit, and event handlers.

func (Ctx) Query

func (c Ctx) Query(name string) string

Query returns the first value of the named URL query parameter.

func (Ctx) Session

func (c Ctx) Session() string

Session returns the opaque liquid_session ID the request runs under (D15, D18), or "" for a session-less request. On the very first render of an interactive page the ID was minted mid-request, so it comes from the framework rather than the not-yet-set cookie.

type Derived

type Derived[T any] struct {
	// contains filtered or unexported fields
}

Derived is a value computed from one or more upstream Observables. It reads and subscribes exactly like a BehaviorSubject — it is backed by one — so it composes with Observe, server push (D3), patch swaps (D14), and further combinators with no wire-format change (D25). It is inert until observed: the framework activates its internal subscriptions through the same Subscription lifecycle that owns a component's bindings, reference-counted so several observers share one set of upstream subscriptions and the last release tears them down. That is the point of D25 — the leak-prone subscription lifecycle lives behind the framework boundary, never in generated tile code.

func CombineLatest

func CombineLatest[A, B, C any](a Observable[A], b Observable[B], fn func(A, B) C) *Derived[C]

CombineLatest recomputes fn from the latest value of both inputs whenever *either* one changes. It is the load-bearing combinator of D25: one filter control (a date range, an environment selector) fans out to every dependent tile without the author wiring N→M subscriptions by hand. Each recompute reads a.Value()/b.Value() rather than the emitted value, so it always combines current state regardless of which input fired. Both subscriptions are framework-owned and reaped together with the observing session.

func Interval

func Interval[T any](ctx context.Context, period time.Duration, fn func() T) *Derived[T]

Interval exposes a periodic poll as a stream: fn runs once now to seed the value, then again on every period tick, each result emitted to observers. The poll goroutine is tied to the observing session's lifetime exactly like a subscription pump — it does not run until the source is observed, and it stops on the last release or when ctx is cancelled, whichever comes first (D25). There is no unbounded background work: a source nobody observes never starts a goroutine, and one that is observed is reaped with its session.

func Map

func Map[T, U any](src Observable[T], fn func(T) U) *Derived[U]

Map is a 1→1 projection: every value of src becomes fn(value) in the derived value. The subscription to src is framework-owned — reaped when the observing session goes (D25) — so generated tiles never manage it.

func Throttle

func Throttle[T any](src Observable[T], window time.Duration) *Derived[T]

Throttle is backpressure for a chatty source: it samples src at most once per window, so a burst of emissions collapses to a single downstream one carrying the latest value — not every tick becomes an SSE patch (D25). It samples rather than debounces, so a source that never goes quiet still advances once per window. The upstream subscription and the sampling goroutine are framework-owned and reaped with the observing session.

func (*Derived[T]) Subscribe

func (d *Derived[T]) Subscribe(fn func(T)) (cancel func())

Subscribe follows the derived value's emissions, exactly as for a subject. Subscribing does not itself start the upstream wiring — activate does, driven by Observe — so lifecycle stays owned by the framework, not by whoever reads the value.

func (*Derived[T]) Value

func (d *Derived[T]) Value() T

Value returns the derived value's current computed value.

type Envelope

type Envelope struct {
	Patch    string `json:"patch,omitempty"`
	Redirect string `json:"redirect,omitempty"`
	CSRF     string `json:"csrf,omitempty"`
}

Envelope is the hydro event response (D19): an HTML patch to swap at the [hydroId] boundary, or a redirect for the runtime to navigate to. A patch answer also re-mints the render's CSRF token (D15, #46) so a long-open interactive page tracks the session's sliding idle deadline instead of the original render's fixed horizon; it is empty on a redirect answer.

type Errors added in v1.0.0

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

Errors is an ordered collection of per-field validation errors. A typed payload's Validate method returns one; a component renders it through a field of this type, which the framework fills on a failed submit and clears on a successful dispatch. The zero value is an empty, ready-to-use set.

func (*Errors) Add added in v1.0.0

func (e *Errors) Add(field, message string)

Add appends a validation error for field with message. Add has a pointer receiver, so build errors on an addressable value — the idiomatic `var errs liquid.Errors; errs.Add(...)` inside a Validate method.

func (Errors) All added in v1.0.0

func (e Errors) All() []FieldError

All returns every error in insertion order, for iterating the whole set.

func (Errors) Any added in v1.0.0

func (e Errors) Any() bool

Any reports whether the set holds any error — the template guard `{{ if .Errors.Any }}`.

func (Errors) For added in v1.0.0

func (e Errors) For(field string) []string

For returns the messages recorded for a field, in order, or nil when it has none — the per-field template accessor `{{ range .Errors.For "Email" }}<p>{{ . }}</p>{{ end }}`.

func (Errors) Len added in v1.0.0

func (e Errors) Len() int

Len returns the number of errors.

type Event

type Event struct {
	Ctx
	// contains filtered or unexported fields
}

Event is the payload handed to a func(e liquid.Event) handler (D11): typed accessors over the fields the runtime script posted with the event — for a (submit), the serialized form — plus the request Ctx (D18) through embedding. Handlers needing no payload stay func().

func (Event) Bind

func (e Event) Bind(dst any) error

Bind fills dst — a pointer to a struct — from the event's payload. Each payload field is matched to an exported struct field by case-insensitive name; matched string fields copy verbatim and int fields parse, with a parse failure reported as an error naming the payload field. Payload fields without a matching struct field (the auto-injected csrf_token, say) are ignored, as are struct fields the payload does not mention.

func (Event) Int

func (e Event) Int(name string) int

Int returns the named payload field parsed as an integer, or 0 when the field is absent or not a number; use Bind to surface parse failures.

func (Event) Redirect

func (e Event) Redirect(path string)

Redirect answers the event with a client-side navigation to path instead of an HTML patch (D19). The last call wins; state mutated before the call still happens, it just isn't re-rendered.

func (Event) String

func (e Event) String(name string) string

String returns the named payload field, or "" when the event carries none.

type FieldError added in v1.0.0

type FieldError struct {
	Field   string
	Message string
}

FieldError is one validation failure: the payload field it concerns and a human-readable message. A field with no natural name (a form-wide error) may use the empty string.

type Guard

type Guard func(ctx Ctx) GuardResult

Guard decides whether a request may activate a route (CanActivate, D4). Guards run after route matching and before the component is instantiated.

func RequireAuthenticated added in v1.0.0

func RequireAuthenticated() Guard

RequireAuthenticated is a guard that denies (403) a request with no verified principal (#108). Compose it with WithGuard; write role/permission guards by reading ctx.Principal() directly.

func RequireAuthenticatedElse added in v1.0.0

func RequireAuthenticatedElse(path string) Guard

RequireAuthenticatedElse is RequireAuthenticated that redirects an anonymous request to path (a login route) instead of denying (D19).

type GuardResult

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

GuardResult is a guard's verdict: allow, deny, or redirect (D19).

func Allow

func Allow() GuardResult

Allow lets the request proceed to the component.

func Deny

func Deny() GuardResult

Deny blocks the request with 403 Forbidden.

func Redirect

func Redirect(path string) GuardResult

Redirect blocks the request and sends the client to path instead — the login-flow variant of a denial (D19).

type Head struct {
	Title string
	Meta  []Meta
}

Head is a page's document head: the <title> plus any named meta tags (D22). Values pass through html/template's contextual escaping like all component state.

type HeadProvider

type HeadProvider interface {
	Head() Head
}

HeadProvider is implemented by components that control their document head (D22). Head runs on the fresh per-request instance after OnInit, so per-request state may inform the title. Components without it fall back to their selector as the title.

type Initializer

type Initializer interface {
	OnInit(ctx Ctx) error
}

Initializer is implemented by components that load per-request state before render (D18). OnInit runs on the fresh per-request instance, after path-param binding and guards; an error return produces the framework error page instead of a render.

type Limits

type Limits struct {
	// MaxSessions caps live sessions across the App. At the cap, minting a
	// new session evicts the oldest. Default DefaultMaxSessions.
	MaxSessions int
	// MaxComponentsPerSession caps live component instances under one
	// session. At the cap, registering a new instance evicts the session's
	// oldest. Default DefaultMaxComponentsPerSession.
	MaxComponentsPerSession int
	// SessionIdleTimeout is how long a session may go without a request
	// before it expires and its live instances are dropped (D2). CSRF token
	// expiry tracks the same window (D15). Default
	// DefaultSessionIdleTimeout.
	SessionIdleTimeout time.Duration
	// MaxEventBytes caps the /hydro-event request body, enforced while the
	// body is read — an oversized event is refused with 413 without being
	// parsed. Default DefaultMaxEventBytes.
	MaxEventBytes int64
	// MaxStreamsPerSession caps open SSE connections under one session. At
	// the cap, a new connection disconnects the session's oldest — the
	// dropped browser reconnects into a full re-render (D20). Default
	// DefaultMaxStreamsPerSession.
	MaxStreamsPerSession int
}

Limits bounds the in-memory session registry (D20). The registry is always bounded: a zero or negative field means its documented default, and there is no unlimited setting.

type LoadOption

type LoadOption func(*Loader)

LoadOption configures one Loader in a Ctx.Fanout call.

func WithTimeout

func WithTimeout(d time.Duration) LoadOption

WithTimeout bounds one loader: its function sees a context with this deadline (in addition to the request's own cancellation). The deadline is cooperative — a function that ignores its context is not killed, it just delays Fanout's return.

type Loader

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

Loader is one concurrent data source in a Ctx.Fanout call. Build it with Load; the zero value is not useful.

func Load

func Load[T any](dst *T, fn func(ctx context.Context) (T, error), opts ...LoadOption) Loader

Load pairs a destination field with the function that produces its value, for use with Ctx.Fanout. fn's result is stored in *dst only on success.

type Meta

type Meta struct {
	Name    string
	Content string
}

Meta is one named <meta> tag, e.g. {Name: "description", Content: "…"}.

type Metrics added in v1.0.0

type Metrics interface {
	// PageRendered reports one page (GET route) response: the HTTP status
	// written — 200, or 500 on a render failure — and how long rendering took.
	PageRendered(status int, dur time.Duration)
	// EventDispatched reports one /hydro-event response: the status the dispatch
	// seam returned — 200 on a handled event, or the refusal code (400 payload,
	// 403 CSRF, 404 unknown session/action, 405, 413, 500) — and how long
	// handling took.
	EventDispatched(status int, dur time.Duration)
	// StreamOpened and StreamClosed bracket one live SSE connection, so their
	// running difference is the current open-stream count. Dev-only streams are
	// not reported.
	StreamOpened()
	StreamClosed()
}

Metrics receives observability events from a running App: page renders, interactive-event dispatches, and live SSE connections. It is the seam a deployment maps onto Prometheus, OpenTelemetry, statsd, or a log — the App itself pulls in no metrics dependency. The zero configuration is a no-op.

Implementations must be safe for concurrent use and must not block: an event is delivered on the request's own goroutine, so slow work here slows the request. Aggregate cheaply (increment a counter, observe a histogram) and do any export out of band.

type Notification added in v1.1.0

type Notification struct {
	Title string `json:"title"`
	Body  string `json:"body"`
}

Notification is a native OS notification request.

type Observable

type Observable[T any] interface {
	// Value returns the current value synchronously.
	Value() T
	// Subscribe registers fn for future emissions and returns its cancel. The
	// current value is not replayed — read it with Value.
	Subscribe(fn func(T)) (cancel func())
}

Observable is the read-and-subscribe surface shared by BehaviorSubject and the derived combinators (Map, CombineLatest, Interval, Throttle). Observe and every combinator take an Observable, so a derived value composes over either a plain subject or another derived exactly alike, with no wire-format or transport change (D25).

type Option

type Option func(*App)

Option configures an App at construction.

func WithLimits

func WithLimits(l Limits) Option

WithLimits sets the App's session-registry and request bounds (D20). Unset (zero) fields keep their documented defaults; without this option every limit is at its default.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the slog logger the App uses for runtime errors. Without it, the App logs through slog.Default().

func WithMetrics added in v1.0.0

func WithMetrics(m Metrics) Option

WithMetrics installs the observability sink for page renders, event dispatches, and SSE connections. Without it, the App records nothing. A nil argument is ignored, leaving the no-op default in place.

type PayloadDomainProvider

type PayloadDomainProvider interface {
	PayloadDomains() map[string]map[string][]string
}

PayloadDomainProvider is implemented by generated code that carries the D30 closed-domain constraints an action's payload struct declares: per action name, the (case-folded) payload field mapped to the enumerated set of values the dispatch seam admits. Reflection cannot see a Go const-set, so the compiler enumerates it via go/types and emits it here for the seam to enforce (a value outside the set is refused 400, before the handler). A component with no closed-domain payload field never generates this method.

type RouteOption

type RouteOption func(*route)

RouteOption configures one route at registration.

func WithGuard

func WithGuard(g Guard) RouteOption

WithGuard adds a CanActivate guard to the route. Guards run in the order they were added; the first non-allow verdict decides the response.

type ServeConfig added in v1.0.0

type ServeConfig struct {
	// Addr is the TCP listen address, e.g. ":8080". Required by App.Serve.
	Addr string

	// ReadHeaderTimeout bounds how long reading request headers may take — the
	// guard against a Slowloris client holding a connection open. Default 5s.
	ReadHeaderTimeout time.Duration

	// IdleTimeout bounds how long an idle keep-alive connection is kept open.
	// Default 2m. It does not apply to an active SSE response, which is a
	// long-lived write rather than an idle connection.
	IdleTimeout time.Duration

	// ShutdownTimeout bounds graceful shutdown: once the context is cancelled,
	// Serve stops accepting connections, closes live SSE streams so their
	// handlers return, and waits up to this long for in-flight requests to
	// finish. Default 15s.
	ShutdownTimeout time.Duration

	// TLSCertFile and TLSKeyFile, when both are set, serve HTTPS. Leaving them
	// empty and terminating TLS at an upstream reverse proxy is equally
	// supported.
	TLSCertFile string
	TLSKeyFile  string
}

ServeConfig configures App.Serve. The zero value is usable: an unset field takes its documented default.

type Subscription

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

Subscription is one declared binding from a subject to an interactive component, built with Observe and returned from Subscriptions. It is inert on its own: the framework activates it after the render registers the live instance, and cancels it when the session's registry entry goes (D20) — components never manage the underlying subscription themselves.

func Observe

func Observe[T any](src Observable[T], apply func(T)) Subscription

Observe declares that an interactive component follows an observable: every emission runs apply (typically assigning to a field) and pushes the re-rendered component over the session's SSE stream (D3). apply receives the source's current value at render time, which under rapid emissions may skip intermediate values — pushes carry latest state, not history (D20).

The source is any Observable, so a component observes a plain subject or a derived combinator (Map, CombineLatest, Interval, Throttle) identically. If the source is a derived value, observing it also activates its internal upstream wiring, and the cancel returned to the pump tears that wiring down with the binding — so a derived value's leak-prone subscriptions live and die with the session's registry entry, never in generated code (D25).

type SubscriptionProvider

type SubscriptionProvider interface {
	Subscriptions() []Subscription
}

SubscriptionProvider is implemented by interactive components that follow subjects. Subscriptions runs on the fresh per-request instance after OnInit; the returned bindings live exactly as long as the instance's registry entry.

type Validator added in v1.0.0

type Validator interface {
	Validate() Errors
}

Validator is implemented by a typed payload that wants server-side validation (#105). The seam calls Validate after binding the wire payload and before the handler; a non-empty result skips the handler and re-renders with the errors. Validate is arbitrary Go over the bound value — the framework prescribes no rule DSL (consistent with D30's Go-predicate guards).

Jump to

Keyboard shortcuts

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