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 ¶
- New constructs an App; App.Route, App.Register, App.Provide, and App.Static wire it up, and App.ServeHTTP serves it.
- Ctx is the per-request context handed to guards and lifecycle hooks; Event is the payload handed to an interactive handler.
- Head, BehaviorSubject, Guard, Load, and the Observable combinators cover state, guarding, and data loading.
Configure the app with Option values such as WithLimits and WithLogger. Logging goes through log/slog with a pluggable handler.
Index ¶
- Constants
- func Notify(ctx context.Context, n Notification) error
- type ActionProvider
- type App
- func (a *App) LiveSessions() int
- func (a *App) Provide(svc any) error
- func (a *App) Register(c Component) error
- func (a *App) Route(path string, c Component, opts ...RouteOption) error
- func (a *App) Serve(ctx context.Context, cfg ServeConfig) error
- func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (a *App) Static(dir string) error
- type BehaviorSubject
- type Component
- type Ctx
- func (c Ctx) Fanout(loaders ...Loader) error
- func (c Ctx) Header(name string) string
- func (c Ctx) Login(principal string) error
- func (c Ctx) Logout() error
- func (c Ctx) Param(name string) string
- func (c Ctx) Principal() (string, bool)
- func (c Ctx) Query(name string) string
- func (c Ctx) Session() string
- type Derived
- func CombineLatest[A, B, C any](a Observable[A], b Observable[B], fn func(A, B) C) *Derived[C]
- func Interval[T any](ctx context.Context, period time.Duration, fn func() T) *Derived[T]
- func Map[T, U any](src Observable[T], fn func(T) U) *Derived[U]
- func Throttle[T any](src Observable[T], window time.Duration) *Derived[T]
- type Envelope
- type Errors
- type Event
- type FieldError
- type Guard
- type GuardResult
- type Head
- type HeadProvider
- type Initializer
- type Limits
- type LoadOption
- type Loader
- type Meta
- type Metrics
- type Notification
- type Observable
- type Option
- type PayloadDomainProvider
- type RouteOption
- type ServeConfig
- type Subscription
- type SubscriptionProvider
- type Validator
Constants ¶
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).
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 (*App) LiveSessions ¶ added in v1.0.0
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 ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
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) Login ¶ added in v1.0.0
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
Logout clears the identity cookie and rotates the session. Event-handler only, like Login.
func (Ctx) Param ¶
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
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.
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 ¶
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.
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
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
Any reports whether the set holds any error — the template guard `{{ if .Errors.Any }}`.
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 ¶
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 ¶
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.
type FieldError ¶ added in v1.0.0
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
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 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 ¶
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 ¶
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.
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
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 ¶
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 ¶
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
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 ¶
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).