Documentation
¶
Overview ¶
Package via builds reactive web UIs from typed Go structs.
A composition is a struct. Its fields declare reactive state (Signal[T], StateTab[T]) and path parameters (path:"name" tag). Its methods of signature func(*Ctx) error become server actions. View(*CtxR) h.H draws it (the render context is read-only, so a View cannot mutate state).
type Counter struct {
Hits via.StateTab[int]
Step via.Signal[int] `via:"step,init=1"`
}
func (c *Counter) Inc(ctx *via.Ctx) error {
c.Hits.Update(ctx, func(n int) (int, error) { return n + c.Step.Read(ctx), nil})
return nil
}
func (c *Counter) View(ctx *via.CtxR) h.H { ... }
app := via.New()
via.Mount[Counter](app, "/counter")
http.ListenAndServe(":3000", app)
Index ¶
- Variables
- func BroadcastSignal[T any](a *App, sig *Signal[T], value T) int
- func Computed(key, expr string) h.H
- func DecodeForm[T any](ctx *Ctx, dst *T)
- func Effect(expr string) h.H
- func Mount[C any](target Mountable, route string)
- func RegisterEvent[E any](fromVersion int, upcast Upcaster)
- func RegisterSnapshotMigration[V any](fromCodecHash string, migrate func([]byte) (V, error))
- func RequestIDFrom(r *http.Request) string
- func RequestWithCSPNonce(r *http.Request, nonce string) *http.Request
- func RequestWithID(r *http.Request, id string) *http.Request
- func RouteFrom(r *http.Request) string
- type Action
- type App
- func (a *App) AppendAttrToHTML(attrs ...h.H)
- func (a *App) AppendToFoot(elements ...h.H)
- func (a *App) AppendToHead(elements ...h.H)
- func (a *App) Broadcast(script string) int
- func (a *App) BroadcastNotify(message string) int
- func (a *App) BroadcastSignals(values map[string]any) int
- func (a *App) Compositions() []CompositionInfo
- func (a *App) EraseDataSubject(ctx context.Context, subject string) error
- func (a *App) Group(prefix string) *Group
- func (a *App) HTTPServer() *http.Server
- func (a *App) Handle(pattern string, handler http.Handler)
- func (a *App) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
- func (a *App) HandleStatic(prefix string, fsys fs.FS)
- func (a *App) LiveTabs() int
- func (a *App) Logger() Logger
- func (a *App) RegisterAppSignal(key string, value any)
- func (a *App) Routes() []RouteInfo
- func (a *App) Run() error
- func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (a *App) Shutdown(ctx context.Context) error
- func (a *App) Start()
- func (a *App) Use(mw ...Middleware)
- type Backplane
- type BoolOps
- type Compactor
- type Composition
- type CompositionInfo
- type Connector
- type ConsumerOption
- type Ctx
- func (ctx *Ctx) CSPNonce() string
- func (ctx *Ctx) Cookie(name string) string
- func (ctx *Ctx) DelCookie(name string)
- func (ctx *Ctx) Disposed() bool
- func (ctx *Ctx) Done() <-chan struct{}
- func (ctx *Ctx) ExecScript(s string)
- func (ctx *Ctx) ID() string
- func (ctx *Ctx) MultipartReader() (*multipart.Reader, error)
- func (ctx *Ctx) Notify(message string)
- func (ctx *Ctx) Patch() *Patch
- func (ctx *Ctx) Redirect(url string)
- func (ctx *Ctx) Reload()
- func (ctx *Ctx) Request() *http.Request
- func (ctx *Ctx) Session() *Session
- func (ctx *Ctx) SetCookie(c *http.Cookie)
- func (ctx *Ctx) SyncNow()
- func (ctx *Ctx) SyncOff()
- func (ctx *Ctx) Writer() http.ResponseWriter
- type CtxR
- type DataSubject
- type Disposer
- type Epoch
- type EventLog
- type EventReducer
- type File
- type Files
- type Group
- type Initializer
- type KeyStore
- type LocalSignal
- type LogLevel
- type Logger
- type LoggerFunc
- type MapOps
- type Metrics
- type Middleware
- type Mountable
- type NumOps
- type Number
- type Offset
- type Option
- func WithActionErrorHandler(fn func(*Ctx, error)) Option
- func WithAddr(addr string) Option
- func WithBackplane(b Backplane) Option
- func WithContextTTL(d time.Duration) Option
- func WithDescription(d string) Option
- func WithFoldVerify() Option
- func WithHTTPServer(hook func(*http.Server)) Option
- func WithIdleTimeout(d time.Duration) Option
- func WithInsecureCookies() Option
- func WithKeyStore(ks KeyStore) Option
- func WithLang(lang string) Option
- func WithLogLevel(level LogLevel) Option
- func WithLogger(l Logger) Option
- func WithMaxContexts(n int) Option
- func WithMaxRequestBody(n int64) Option
- func WithMaxSessions(n int) Option
- func WithMaxUploadSize(n int64) Option
- func WithMetrics(m Metrics) Option
- func WithNotFound(h http.Handler) Option
- func WithPlugins(plugins ...Plugin) Option
- func WithReadHeaderTimeout(d time.Duration) Option
- func WithReadTimeout(d time.Duration) Option
- func WithReconcileInterval(d time.Duration) Option
- func WithRequestTooLarge(h http.Handler) Option
- func WithSSEHeartbeat(d time.Duration) Option
- func WithSSEWriteTimeout(d time.Duration) Option
- func WithSecureCookies() Option
- func WithSessionCookieName(name string) Option
- func WithSessionTTL(d time.Duration) Option
- func WithShutdownTimeout(d time.Duration) Option
- func WithSnapshotInterval(n int) Option
- func WithStrictDecode() Option
- func WithTitle(title string) Option
- func WithVerboseErrors() Option
- func WithWriteTimeout(d time.Duration) Option
- func WithoutDevChecks() Option
- func WithoutHealthEndpoints() Option
- func WithoutSSEReconnect() Option
- type Patch
- type Plugin
- type Record
- type Rev
- type RouteInfo
- type Session
- type Signal
- func (s *Signal[T]) Attr(name string) h.H
- func (s *Signal[T]) Bind() h.H
- func (s *Signal[T]) Class(name string) h.H
- func (s *Signal[T]) Key() string
- func (s *Signal[T]) Read(_ readCtx) T
- func (s *Signal[T]) Show() h.H
- func (s *Signal[T]) ShowUnless() h.H
- func (s *Signal[T]) Style(prop string) h.H
- func (s *Signal[T]) Text() h.H
- func (s *Signal[T]) TextSpan() h.H
- func (s *Signal[T]) Update(ctx *Ctx, fn func(T) (T, error)) error
- func (s *Signal[T]) Write(ctx *Ctx, v T)
- type SignalBool
- type SignalMap
- type SignalNum
- type SignalSlice
- type SignalStr
- type SliceOps
- type StateApp
- type StateAppBool
- type StateAppEvents
- func (l *StateAppEvents[E, V]) Append(ctx *Ctx, ev E) (Offset, error)
- func (l *StateAppEvents[E, V]) Key() string
- func (l *StateAppEvents[E, V]) OnEvent(name string, fn func(ctx context.Context, ev E, off Offset) error, ...)
- func (l *StateAppEvents[E, V]) Read(rc readCtx) V
- func (l *StateAppEvents[E, V]) Text(rc readCtx) h.H
- type StateAppMap
- type StateAppNum
- type StateAppSlice
- type StateAppStr
- type StateSess
- type StateSessBool
- type StateSessMap
- type StateSessNum
- type StateSessSlice
- type StateSessStr
- type StateTab
- type StateTabBool
- type StateTabMap
- type StateTabNum
- type StateTabSlice
- type StateTabStr
- type Store
- type StrOps
- type Ticker
- type Upcaster
Constants ¶
This section is empty.
Variables ¶
var ( // ErrCASConflict is returned by Store.CAS when the current revision moved // since the caller's expectedRev — reload and retry. ErrCASConflict = errors.New("via: store CAS revision conflict") // ErrClosed is returned by a closed backplane's Append/Subscribe. ErrClosed = errors.New("via: backplane closed") // ErrUndecodable: an event record has no viable decode path to the current // version (corrupt bytes, missing envelope, or no upcaster). The projector // treats the record as a no-op (skips it, advancing past it) and emits // via.events.undecodable — a poison record must never panic the pod and // wedge every peer replaying the key. ErrUndecodable = errors.New("via: no decode path to the current event version") // ErrForwardIncompatible: a record's envelope version exceeds what this // binary understands (a rolled-back deploy reading newer events). The key's // projector HALTS rather than silently mis-fold — roll forward, not back. ErrForwardIncompatible = errors.New("via: event version newer than this binary supports; roll forward, not back") )
var ErrErased = errors.New("via: event encrypted under an erased data-subject key (crypto-shredded)")
ErrErased: an event's payload was encrypted under a per-data-subject key that has since been DROPPED (a GDPR erasure / crypto-shred). The plaintext is unrecoverable by design. The projector and consumers treat it like a poison record — skip it, advance past it — but emit via.events.erased instead of via.events.undecodable, because this is an intentional erasure, not corruption.
Functions ¶
func BroadcastSignal ¶ added in v0.4.1
BroadcastSignal pushes one typed signal value to every currently-live tab via its Signal[T] handle — the typed counterpart of App.BroadcastSignals for signals bound at Mount. Returns the tab count; nil sig is a no-op.
func Computed ¶ added in v0.6.0
Computed defines a client-side derived signal named key whose value is recomputed from expr whenever any signal expr references changes. It maps to Datastar's data-computed-<key>; expr is a JS expression over other signals (referenced as $other). Derived values stay on the client — no server round-trip — so prefer it over re-deriving in View for display-only values (totals, formatted labels, validity flags):
via.Computed("full", "$first + ' ' + $last")
h.Span(c.Full.Text()) // $full is now bindable like any signal
expr is emitted verbatim (HTML-escaped); it is a raw Datastar expression, not a typed Go value — reference signals by their wire key with a $ prefix.
func DecodeForm ¶ added in v0.4.0
DecodeForm parses the request body's signal payload into a typed struct. Datastar action POSTs send signals as JSON; this helper lets actions pull a strongly-typed view of selected signals into a struct, decoded by `form:"name"` tags. Useful when the action wants a value-object instead of N separate Signal[T].Read(ctx) calls.
type LoginForm struct {
Email string `form:"email"`
Password string `form:"password"`
}
func (p *Page) Submit(ctx *via.Ctx) error {
var f LoginForm
via.DecodeForm(ctx, &f)
...
}
Source resolution: the helper first checks the action's signal payload (read off r at dispatch and stored on ctx), then falls back to r.URL.Query() and r.PostForm if present. Tag-less fields use the lower-cased field name as the key.
Decoding is best-effort: missing keys and unparseable values leave the field at its zero value rather than failing the request. Validate the populated struct in the caller if you need rejection semantics.
func Effect ¶ added in v0.6.0
Effect runs expr reactively on the client whenever a signal it references changes, for side effects (logging, focus, third-party calls) rather than producing a value. It maps to Datastar's data-effect. expr is a raw Datastar expression emitted verbatim (HTML-escaped).
func Mount ¶ added in v0.4.0
Mount registers a typed composition C at route on target.
target may be an *App (route is taken as-is) or a *Group (route is joined under the group's prefix; the group's middleware chain wraps the rendered route + action POST + SSE handshake).
via.Mount[Counter](app, "/counter")
api := app.Group("/api")
api.Use(requireAuth)
via.Mount[Profile](api, "/profile")
C must be a struct whose pointer type satisfies the Composition interface (i.e. has a View(ctx *Ctx) h.H method). Reflection runs once at Mount time to:
- validate View, OnInit, OnConnect, OnDispose signatures (panics with a format-the-fix-yourself message on a mismatch);
- collect Signal[T] / StateTab[T] / StateSess[T] / StateApp[T] fields and assign their wire keys (lowercased field name, or `via:"name"` tag override);
- collect path:"name" / query:"name" tagged fields;
- enumerate exported methods of signature func(*Ctx) error or func(*Ctx) and register them as actions.
Per-request handlers do no reflection on the hot path for already- bound state. Mount panics if the route conflicts with an earlier registration on the same App.
func RegisterEvent ¶ added in v0.5.0
RegisterEvent registers a single-step upcaster that migrates a version fromVersion payload of event type E to version fromVersion+1, so a stored record written before a RESHAPE still decodes into the current E. Call it at init/setup, before Mount. The current version of E becomes 1+max(fromVersion registered) (1 with none).
Additive-first: you rarely need an upcaster. Adding a field needs none (JSON ignores unknown fields on decode and zero-fills missing ones); only a reshape — renaming, splitting, or changing the type of a field — needs one. The upcaster works on raw JSON and never touches Fold, so version logic stays at the codec boundary.
func RegisterSnapshotMigration ¶ added in v0.5.0
RegisterSnapshotMigration teaches the runtime how to bridge a COMPACTED key's durable-genesis snapshot across a change to the projected type V.
Once a key has compacted, the discarded event prefix is unrecoverable, so a snapshot codec-hash mismatch can no longer be resolved by re-folding from genesis — the snapshot IS the genesis. Register, keyed by the PREVIOUS V codec hash (`reflect.TypeFor[OldV]().String()`), a function that decodes the old snapshot bytes into the current V; on cold start the runtime seeds from it and folds the retained tail on top. A compacted key whose old hash has no registered migration (or whose migration errors) HALTS its projector (roll-forward-only) rather than silently truncate to the surviving tail.
Uncompacted keys never consult the registry — their snapshot stays a pure disposable cache (mismatch → re-fold from genesis), so evolving V is free in the common case. Register at init, before the App mounts.
func RequestIDFrom ¶ added in v0.4.0
RequestIDFrom pulls the request id out of r.Context. Returns "" if no RequestID middleware (mw.RequestID) has run for this request. Log uses this to stamp the rid on every record emitted from an action / handler whose request is tagged.
func RequestWithCSPNonce ¶ added in v0.4.0
RequestWithCSPNonce returns r with nonce stored in its context so downstream renderPage can find it via Ctx.CSPNonce. Use it from a custom CSP middleware (or mw.CSP) so the rendered HTML's nonce stays in lock-step with whatever value the middleware puts in the response header.
func RequestWithID ¶ added in v0.4.0
RequestWithID returns r with id planted on its context so downstream handlers can read it via RequestIDFrom. Used by mw.RequestID (and by custom RequestID-shaped middleware) to keep the rid lookup path consistent across packages.
func RouteFrom ¶ added in v0.6.0
RouteFrom returns the resolved logical route of the composition serving this request — the mounted pattern (e.g. "/users/{id}"), the SAME value on the page GET, the action POST, and the SSE handshake. Group middleware can use it to tell which page it is guarding, since on the action/SSE paths r.URL.Path is the shared "/_action/{id}" or "/_sse", not the page route. Returns "" outside a via composition request (e.g. a plain HandleFunc route).
Types ¶
type Action ¶ added in v0.4.1
Action constrains the bound-method shapes the via/on helpers accept: func(*Ctx) error, or func(*Ctx) when nothing in the body can fail. It is a type-parameter constraint (a union), so passing anything else to on.Click and friends is a compile error rather than a runtime panic. The value must still be a bound method value (e.g. c.Inc) — closures and top-level functions satisfy the type but have no method name to route to, and panic at first render.
type App ¶ added in v0.2.0
type App struct {
// contains filtered or unexported fields
}
App is the root of a via web app. It implements http.Handler so it can be passed straight to http.ListenAndServe or composed inside any std mux:
app := via.New()
via.Mount[Counter](app, "/counter")
http.ListenAndServe(":3000", app)
// or, embed under a parent mux:
parent := http.NewServeMux()
parent.Handle("/", app)
func (*App) AppendAttrToHTML ¶ added in v0.2.0
AppendAttrToHTML adds attributes to the <html> element of every page. Boot-only: panics if called after Start has bound the server.
func (*App) AppendToFoot ¶ added in v0.2.0
AppendToFoot adds nodes to the end of <body> on every rendered page. Boot-only: panics if called after Start has bound the server.
func (*App) AppendToHead ¶ added in v0.2.0
AppendToHead adds nodes to the <head> of every rendered page. Call during boot (e.g. from a plugin's Register).
Boot-only: panics if called after Start has bound the server. The read-side (page rendering) is lock-free for speed, so post-boot mutations would race with concurrent renders.
func (*App) Broadcast ¶ added in v0.4.0
Broadcast queues a JavaScript snippet on every currently-live tab's patch queue. The next SSE drain on each tab pushes it to the browser. Useful for "page will reload in 30 seconds" maintenance notices, site-wide flash messages, or coordinated state invalidation.
app.Broadcast(`alert("Maintenance in 30 seconds.")`)
time.Sleep(30 * time.Second)
app.Shutdown(ctx)
When a backplane is wired (via WithBackplane) the script also rides the shared feed and reaches every live tab on every pod; otherwise it stays pod-local. Either way the returned count is THIS pod's live-tab count at call time — the cluster-wide total is unknowable synchronously. Empty script is a no-op.
EXPERIMENTAL: the single-process behavior is stable; the cross-pod semantics (the BroadcastNotify / BroadcastSignals family included) ride the pre-GA backplane and may change before 1.0.
func (*App) BroadcastNotify ¶ added in v0.6.1
BroadcastNotify shows an XSS-safe notification on every currently-live tab — the safe form of App.Broadcast for the common site-wide-notice case, so callers never hand-build (and mis-escape) the notification JS. message is JSON-encoded, so arbitrary text including markup is inert. Like Broadcast it reaches every pod when a backplane is wired and stays pod-local otherwise; the returned count is this pod's live-tab count. Empty message is a no-op.
func (*App) BroadcastSignals ¶ added in v0.4.0
BroadcastSignals pushes a signal patch to every currently-live tab. Useful for site-wide announcements that drive a banner via a client-only signal (e.g. "$_systemNotice = 'planned maintenance'") without rendering each composition.
Like App.Broadcast, the patch rides the shared feed to every pod when a backplane is wired and stays pod-local otherwise; the returned count is this pod's live-tab count at call time.
This is the untyped escape hatch for dynamic / client-only signal keys; when a *Signal[T] handle exists, prefer BroadcastSignal.
func (*App) Compositions ¶ added in v0.4.0
func (a *App) Compositions() []CompositionInfo
Compositions returns a sorted snapshot of the names of every typed Composition mounted on this app, paired with its route. Useful for boot logging or status pages:
for _, c := range app.Compositions() {
log.Printf("mounted %-30s at %s", c.Type, c.Route)
}
func (*App) EraseDataSubject ¶ added in v0.5.0
EraseDataSubject performs a GDPR crypto-shred: it drops the subject's key (so every ciphertext for them in the durable log becomes permanently unreadable) and bumps the global erasure generation (so any pod cold-starting from the log re-folds without the now-undecryptable events instead of seeding a pre-erasure snapshot that still holds their plaintext PII).
What is shredded, precisely:
- The LOG is cryptographically shredded: every record for the subject is ciphertext under the dropped key, unrecoverable forever (the strong guarantee — survives append-only logs and backups).
- SNAPSHOTS are invalidated by generation: a pre-erasure snapshot (whose folded V may hold the subject's plaintext) is never seeded into a projection again, and is physically overwritten by post-erasure V on the key's next snapshot write. The plaintext may linger in the snapshot CELL until that next write, so a deployment with strict erasure SLAs must also expire the snapshot store per its retention policy.
Not covered in v1: a pod's already-running in-memory projection still holds the pre-erasure value until it cold-starts / redeploys (it re-folds clean then). A key that has been COMPACTED (its event prefix discarded) cannot be re-folded after erasure — its snapshot is durable genesis — so compaction + erasure of the same key needs snapshot re-encryption, a documented follow-up.
func (*App) HTTPServer ¶ added in v0.4.0
HTTPServer returns an *http.Server configured with the app as its handler and every WithReadTimeout/WithWriteTimeout/WithIdleTimeout/ WithReadHeaderTimeout option applied. Useful when the caller wants to bind directly (TLS, custom listener, ALB sidecar) instead of going through Start. The returned server has no listener attached; the caller drives ListenAndServe / ListenAndServeTLS themselves.
HTTPServer is also what Start uses internally — same defaults.
func (*App) HandleFunc ¶ added in v0.2.2
HandleFunc registers a non-via handler on the app's mux.
func (*App) HandleStatic ¶ added in v0.4.0
HandleStatic serves files under prefix from fsys. Common pattern for shipping a single binary with embedded assets:
//go:embed static
var assets embed.FS
sub, _ := fs.Sub(assets, "static")
app.HandleStatic("/assets/", sub)
The pattern ends with a trailing slash; the prefix is stripped before the file lookup. The handler claims `GET <prefix>` so the route table reflects the registration.
func (*App) LiveTabs ¶ added in v0.4.0
LiveTabs returns the number of currently-registered tab contexts. Useful for ops endpoints (/healthz, /metrics) that want to surface concurrency without scraping internal state. The number is a snapshot — it may have changed by the time the caller reads the return value.
func (*App) Logger ¶ added in v0.4.0
Logger returns the Logger configured on a — either the user's WithLogger, or the default log.Printf-backed implementation when none was set. Records emitted below the App's configured log level (see WithLogLevel) are dropped, matching the behaviour the via runtime applies to its own warnings.
Used by middleware in via/mw to emit access logs and panic reports through the same pipe as the runtime's own warnings.
func (*App) RegisterAppSignal ¶ added in v0.4.0
RegisterAppSignal sets the initial value of a named, app-wide signal. Used by plugins to seed data-signals entries that the client owns (e.g. picocss's "_picoTheme"). The value is JSON-encoded into every page's <meta data-signals> on render.
func (*App) Routes ¶ added in v0.4.0
Routes returns a sorted snapshot of every method+pattern registered on this app, paired with the registrar tag (Mount[T], HandleFunc, Group(prefix).Handle, …). Useful for `app.Routes()` debugging and for surfacing registered surface area at boot.
func (*App) Run ¶ added in v0.6.0
Run binds and serves on the configured address, wiring SIGINT/SIGTERM to a graceful Shutdown. It blocks until the server stops and returns the listen error (nil on a graceful shutdown — http.ErrServerClosed is normalized to nil). Use Run when you want to handle a bind failure (e.g. "address already in use") yourself; use App.Start for the panic-on-error convenience.
func (*App) ServeHTTP ¶ added in v0.4.0
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP makes *App an http.Handler.
func (*App) Shutdown ¶ added in v0.2.0
Shutdown gracefully tears down the app:
- Every live Ctx's Done channel is closed so SSE drain loops and Stream goroutines exit promptly.
- The registry is cleared so concurrent action POSTs that arrive after this point 404 instead of running against a half-disposed Ctx.
- The underlying http.Server is drained (waits for in-flight non-SSE handlers up to ctx's deadline).
- Per-Ctx OnDispose runs, serialized against any in-flight action via the per-Ctx action mutex.
Sessions and the TTL sweeper are torn down last. The error from the http.Server's Shutdown is returned; a wedged OnDispose handler does not propagate but is logged.
func (*App) Start ¶ added in v0.2.0
func (a *App) Start()
Start is the panic-on-error convenience wrapper over App.Run: a bind failure becomes a panic. SIGINT/SIGTERM trigger a graceful Shutdown.
func (*App) Use ¶ added in v0.2.3
func (a *App) Use(mw ...Middleware)
Use installs middleware that wraps every via-served request.
Boot-only: panics if called after Start has bound the server. Concurrent Use calls are safe — the middleware slice and the chain rebuild are serialized under one mutex.
type Backplane ¶ added in v0.5.0
Backplane is the one interface a backend author implements to make app/session-scoped reactive state survive restarts and span a cluster. It fuses a durable per-key CAS Store and a durable ordered EventLog, plus a graceful drain on App.Shutdown. After Close, Append/Subscribe return ErrClosed and never block.
EXPERIMENTAL: the clustered/distributed path is pre-GA. The default InMemory (single-pod) backplane is stable, but this interface and the surrounding guarantees may change before 1.0 — 1.0 does not promise a distributed GA.
func InMemory ¶ added in v0.5.0
func InMemory() Backplane
InMemory returns the base in-process Backplane: a per-key in-memory event log plus a CAS snapshot cell. It is the impl a nil backplane resolves to, so the Backplane interface is exercised on every single-pod run. (See T1-GO-6 for why this lives in package via rather than a separate package.)
type BoolOps ¶ added in v0.4.0
type BoolOps struct {
// contains filtered or unexported fields
}
BoolOps is the chain returned by Op(ctx) on every Bool* reactive type; its boolean verbs route through the handle's Update path.
type Compactor ¶ added in v0.5.0
type Compactor interface {
// Compact reclaims storage for key by discarding every record with
// Offset < beforeOffset. Offsets of RETAINED records are UNCHANGED — the log
// keeps a front hole, and Subscribe(from:0) resumes at the lowest retained
// offset (never renumbered). Idempotent and monotone: a beforeOffset at or
// below what was already compacted is a no-op, and one beyond the committed
// head is clamped to it (the head never moves).
Compact(ctx context.Context, key string, beforeOffset Offset) error
}
Compactor is an OPTIONAL backplane capability. The runtime type-asserts the Backplane to it and calls Compact only AFTER a snapshot covering the discarded prefix is durably written (snapshot-FIRST, compact-SECOND is mandatory: a crash between them re-replays a few events, never loses one). A backend with no native compaction simply omits it and runs snapshot-only.
- JetStream: purge-up-to-sequence / per-subject limits.
- Redis Streams: XTRIM MINID.
- Postgres: DELETE FROM events WHERE key=$1 AND offset<$2 in the snapshot txn.
- Kafka: the event topic can't native-compact (latest-per-key is wrong for a log), so it compacts the snapshot topic + delete-records the event topic, OR declines Compactor and runs snapshot-only.
type Composition ¶ added in v0.4.0
Composition is anything that renders a view from a read-only Ctx. Types whose pointer satisfies this interface are mountable.
type CompositionInfo ¶ added in v0.4.0
type CompositionInfo struct {
Type string // type name, e.g. "via_test.Counter"
Route string // mounted pattern
}
CompositionInfo is one entry in App.Compositions().
type Connector ¶ added in v0.4.0
Connector is the optional lifecycle hook that fires once when the SSE stream first opens for this tab. Bots that hit GET without ever opening the SSE never see this fire, so expensive background work (Stream tickers, fan-out goroutines) belongs here rather than in OnInit. A non-nil error is logged.
type ConsumerOption ¶ added in v0.7.0
type ConsumerOption func(*consumerConfig)
ConsumerOption tunes a single OnEvent consumer's poison/retry policy.
func WithDeadLetter ¶ added in v0.7.0
func WithDeadLetter(fn func(ctx context.Context, key string, off Offset, data []byte, cause error) error) ConsumerOption
WithDeadLetter registers a hook invoked when a record is about to be poisoned (only reachable with WithMaxAttempts(n>0)). It receives the wire key, offset, raw record bytes, and the last handler error. If it returns nil the consumer commits past the record; if it returns an error the consumer does NOT advance and keeps retrying, so a record the operator opted to dead-letter is never silently lost when the sink is unavailable.
func WithMaxAttempts ¶ added in v0.7.0
func WithMaxAttempts(n int) ConsumerOption
WithMaxAttempts opts a consumer into skipping a poison record. After n consecutive handler-error attempts on the same record the consumer treats it as poison (dead-letters it if a hook is set, otherwise drops it) and advances past it, un-wedging the consumer and unpinning the Compactor floor.
The DEFAULT is 0 = block forever: a persistently-failing handler keeps the record head-of-line and never drops the side effect (today's zero-data-loss semantics). Skipping is strictly opt-in via n > 0. A forward-incompatible record (a newer binary wrote it) is NEVER poisoned regardless of n — it always blocks, since it is a rollback guard rather than a bad record.
func WithRetryBackoff ¶ added in v0.7.0
func WithRetryBackoff(base, max time.Duration) ConsumerOption
WithRetryBackoff sets the exponential-backoff bounds (with jitter) the consumer waits between head-of-line retries of a failing handler. Defaults are 10ms → 30s.
type Ctx ¶ added in v0.2.0
type Ctx struct {
// contains filtered or unexported fields
}
Ctx is the per-request execution context. Created on page load, kept alive for the lifetime of the SSE stream, passed to View/OnInit/Action methods.
func (*Ctx) CSPNonce ¶ added in v0.4.0
CSPNonce returns a per-request cryptographically-random base64 nonce suitable for use with strict Content-Security-Policy headers. The same value is returned on every call within one request, so plugins and the page render share one nonce.
For strict CSP enforcement, install mw.CSP — or write your own middleware that pre-generates the nonce, sets the Content-Security- Policy header, and threads it through the request via RequestWithCSPNonce. Without that, CSPNonce returns a random per-request value the browser will not honor.
func (*Ctx) Cookie ¶ added in v0.4.0
Cookie returns the value of the named cookie on the in-flight request, or "" if the cookie isn't present. Convenience over Request().Cookie for the common 80% case where you just want the value:
consent := ctx.Cookie("cookie_consent")
For full cookie access (Path, Expires, …) use Request().Cookie.
func (*Ctx) DelCookie ¶ added in v0.4.0
DelCookie tells the browser to delete the named cookie by emitting a Set-Cookie response header with an empty value, MaxAge=-1, and Path="/". For a cookie scoped to a specific path use SetCookie with a fully-formed *http.Cookie instead.
func (*Ctx) Disposed ¶ added in v0.4.0
Disposed reports whether the Ctx has been torn down (tab closed, swept by ctx-TTL, or app shutdown). Use it from a long-running goroutine to skip expensive work that nobody's going to see:
for {
if ctx.Disposed() { return }
...
}
Equivalent to a non-blocking <-ctx.Done(), but reads more naturally inline.
func (*Ctx) Done ¶ added in v0.2.0
func (ctx *Ctx) Done() <-chan struct{}
Done returns a channel closed on context disposal (tab close or shutdown).
func (*Ctx) ExecScript ¶ added in v0.2.0
ExecScript queues a JavaScript snippet for execution on the client at the next flush. Use sparingly — most reactivity should flow through signals/state rather than imperative scripts.
func (*Ctx) MultipartReader ¶ added in v0.4.0
MultipartReader returns the multipart reader on the in-flight request, or an error if the request body is not multipart. Use this from an action when you need streaming control over a multipart body that the typed via.File field doesn't cover (mixed parts, custom part headers, very large files where ParseMultipartForm's memory buffer is too small). Outside action scope, returns an error.
Calling MultipartReader consumes the body forward-only; once read, typed via.File fields on the same action will be empty for any parts you advance past.
func (*Ctx) Notify ¶ added in v0.6.1
Notify shows message as a transient notification. The default (and currently only) surface is a small, styled, non-blocking toast that slides into a fixed overlay and auto-dismisses after a few seconds. It is the default surface for recovered action-handler panics, and the "show a quick notice and move on" sugar for app code. Zero setup: the first toast on a page injects its own <style> and #via-toast-root container, later toasts reuse them and stack.
The message is rendered via textContent (never as HTML), and JSON- encoded into the snippet so it can neither break out of the JS string nor inject markup — Go's json HTML-escaping also neutralises a </script> breakout of the surrounding datastar script element.
EXPERIMENTAL: the contract (show a transient message) is stable, but the rendered SURFACE — the toast markup, styling, and stacking — may change before 1.0; don't depend on the emitted DOM.
func (*Ctx) Patch ¶ added in v0.4.0
Patch returns the imperative client-push handle for this request — the escape hatch for pushing a signal value to a key not bound to a typed Signal[T], or morphing an arbitrary element fragment into the live DOM. See Patch for the primitives. The handle is allocated eagerly in newCtx, so this is a plain field load that never returns nil for a live ctx.
func (*Ctx) Redirect ¶ added in v0.2.3
Redirect sends a client-side navigation to url at the next flush.
Only http, https, and same-origin relative paths are honoured. URLs carrying any other scheme (javascript:, data:, vbscript:, …) or a protocol-relative // prefix are dropped and logged — this closes the open-redirect / XSS vector when callers interpolate user input into the URL (typical ?next= flows).
func (*Ctx) Reload ¶ added in v0.4.0
func (ctx *Ctx) Reload()
Reload tells the browser to reload the current page on the next flush. Convenience wrapper for the common "the data changed drastically; just refetch" pattern after multi-step actions.
func (*Ctx) Request ¶ added in v0.3.0
Request returns the *http.Request for the in-flight request, or nil if the caller isn't on the action or page-render goroutine. Same lifetime caveat as [Writer]: cleared on handler return, do not capture for later use.
func (*Ctx) Session ¶ added in v0.4.0
Session returns a Session bound to ctx. Stores performed through the returned handle mark the page dirty and fan out to subscribed tabs. Survives tab close; expires per WithSessionTTL.
Typed access lives in the via/sess subpackage — most code reaches for sess.Get[T] / sess.Put[T] / sess.Clear[T] rather than this handle directly.
func (*Ctx) SetCookie ¶ added in v0.4.0
SetCookie writes a cookie on the action's response. Convenience over http.SetCookie that pulls the response writer off the Ctx; safe to call from an action handler. Outside action scope (Writer == nil) it is a no-op.
func (*Ctx) SyncNow ¶ added in v0.4.0
func (ctx *Ctx) SyncNow()
SyncNow forces a view re-render and flushes pending patches now, without waiting for the auto-flush at end of action. Marks the composition dirty even if nothing changed since the last flush — use it when an external (non-State) source of truth changed and you need the rendered HTML to reflect it.
Designed for raw goroutines that mutate Ctx-bound State or Signal values outside an action handler. Safe to call from any goroutine: serialized against in-flight action handlers via the per-Ctx action mutex. Calling from inside an action handler deadlocks (the action holds the mutex); rely on the auto-flush at handler return instead.
func (*Ctx) SyncOff ¶ added in v0.4.0
func (ctx *Ctx) SyncOff()
SyncOff opts the current action handler out of publishing. While off, the deferred end-of-action flush is skipped, accumulated dirty bits are dropped at handler return, and shared-state writes (StateSess/StateApp.Update, Session.Store) skip their in-line broadcast to subscribed sibling tabs. Local State/Signal writes still land in the underlying stores — they just don't reach any browser this action. A later loud action that re-touches the state surfaces the value via the normal dirty-bit path.
Explicit publish primitives (ctx.Patch().{Signal,Signals,Element,Elements}, ExecScript, Notify, Reload, Redirect) are NOT suppressed by SyncOff — they enqueue patches directly rather than through the dirty-bit flush. This is deliberate so a panic-recovery error toast still reaches the user even when the action was running silent.
SyncOff is action-scoped: every action handler, stream tick, and lifecycle hook starts loud. The flag is intentionally not propagated to user-launched goroutines — they observe whatever value the flag holds at the moment they read it.
Use it for try-before-commit flows, bulk reconciliation, composing plugin handlers whose writes you don't want to publish, or any path where partial state must not leak on error. SyncOff is one-way for the duration of the handler — there is no companion to re-enable publishing mid-handler. Structure code so the publish-worthy writes happen in their own loud action, or wait until handler return.
func (*Ctx) Writer ¶ added in v0.3.0
func (ctx *Ctx) Writer() http.ResponseWriter
Writer returns the http.ResponseWriter for the in-flight request, or nil if the caller isn't on the action or page-render goroutine. The pointer is cleared as soon as the synchronous handler returns, so it is unsafe to capture from a background goroutine and use later.
type CtxR ¶ added in v0.4.0
type CtxR struct {
// contains filtered or unexported fields
}
CtxR is the read-only render context passed to View(ctx *CtxR) h.H. It exposes the read-side of the per-tab runtime — the keys, the session, lookups — but withholds every mutator (Set/Update, the publish primitives, SyncNow/SyncOff, response-Writer access). Calls that would change observable state are not on this type, so writing into a View becomes a compile error.
Use *Ctx (not *CtxR) for action handlers, lifecycle hooks, Stream callbacks, and any goroutine that needs to mutate state.
func (*CtxR) CSPNonce ¶ added in v0.4.0
CSPNonce mirrors Ctx.CSPNonce — returns this request's strict-CSP nonce so View can embed it on inline <script>/<style> tags.
func (*CtxR) Cookie ¶ added in v0.4.0
Cookie returns the value of the named cookie on the in-flight request, or "" if absent. Mirrors Ctx.Cookie — safe in View where the page-render request is still live.
func (*CtxR) Session ¶ added in v0.4.0
Session mirrors Ctx.Session — returns a handle bound to this tab's session. Useful for reading session-scoped values during a render. Writes to the returned handle (Store, Delete) still trigger a broadcast; calling them from a View defeats the read-only contract. Prefer sess.Get[T] for typed reads and reserve writes for action handlers / lifecycle hooks that hold *via.Ctx.
type DataSubject ¶ added in v0.5.0
type DataSubject interface {
DataSubject() string
}
DataSubject is implemented by an event type whose payload belongs to a single data subject. When a KeyStore is configured (WithKeyStore), such an event's payload is encrypted under that subject's key at Append, so erasing the subject (App.EraseDataSubject) crypto-shreds it from the durable log. An event that does not implement DataSubject (or returns "") is stored in plaintext.
type Disposer ¶ added in v0.4.0
type Disposer interface {
OnDispose(ctx *Ctx)
}
Disposer is the optional lifecycle hook that fires when the tab's Ctx is torn down — page unload, ctx-TTL sweep, or app shutdown. Release resources, close goroutines, persist final state. Runs under the per-Ctx action mutex so it observes a composition that isn't being mutated by a concurrent handler.
type Epoch ¶ added in v0.5.0
type Epoch uint64
Epoch is the per-key stream GENERATION. Offsets are only unique and monotone WITHIN an epoch; an offset-space reset starts a new epoch. Epoch(0) is the genesis generation. (The in-memory backplane never resets, so it stays at 0.)
type EventLog ¶ added in v0.5.0
type EventLog interface {
// Append commits one opaque record to key's stream and returns its assigned
// Offset. Plain append never conflicts.
Append(ctx context.Context, key string, record []byte) (Offset, error)
// Subscribe streams records for key with Offset > from (so a pod passes its
// last-applied offset and resumes exactly after it), then live-tails. The
// channel closes when ctx is cancelled or the backplane is closed.
Subscribe(ctx context.Context, key string, from Offset) (<-chan Record, error)
// Head returns the current highest committed Offset for key and its current
// Epoch. Offset(0) if the key is empty.
Head(ctx context.Context, key string) (Offset, Epoch, error)
}
EventLog is the durable, ordered, offset-resumable append log.
Guarantees: per-key total order; an Append returns only after the record is committed and assigned its Offset; Subscribe(from:K) yields every committed record with Offset>K, in order, with no gaps, then live-tails. There is NO cross-key ordering — distinct keys are independent aggregates.
type EventReducer ¶ added in v0.5.0
EventReducer constrains E to be its own reducer: the fold lives on the event TYPE, as a single method. The fold SEED — the projected value of an EMPTY log — is the Go zero value of V; there is no Zero() method.
Determinism rule (load-bearing): Fold MUST be a pure function of (acc, receiver) — no clock, no RNG, no I/O, no globals. Two pods replaying the same offset range must converge to the same V. A wall-clock or random input must be carried as a field on E, stamped at Append, never sampled inside Fold. Fold MUST return acc unchanged for an unknown event variant so a pod running old code that tails a new-variant event folds it as a no-op.
type File ¶ added in v0.4.0
type File struct {
// contains filtered or unexported fields
}
File is a typed, request-scoped handle to one uploaded file part. Add it as a field on a composition struct to receive a file from a multipart action POST:
type Page struct {
Avatar via.File `via:"avatar"`
}
func (p *Page) Upload(ctx *via.Ctx) error {
if !p.Avatar.Present() { return nil }
return p.Avatar.Save("/var/uploads/" + p.Avatar.Filename())
}
Wire key defaults to the lower-cased field name; override with the `via:"name"` tag exactly like Signal[T].
Lifecycle: the file handle is bound at action entry from the multipart body and cleared when the action returns. Read, copy, or Save it during the action body — references are not valid afterward.
func (*File) Bytes ¶ added in v0.4.0
Bytes reads the file body into memory and returns it. For large uploads prefer Open + io.Copy to avoid buffering everything at once.
func (*File) ContentType ¶ added in v0.4.0
ContentType returns the Content-Type header the client sent for the part. Untrusted: clients can claim any content type, so use a content-sniffing library (net/http.DetectContentType on the first 512 bytes) before relying on it.
func (*File) Filename ¶ added in v0.4.0
Filename returns the client-supplied filename. Untrusted: never use it as a filesystem path without sanitizing — callers should prefer generating their own name and using Save with that path.
func (*File) Open ¶ added in v0.4.0
Open returns a stream over the file body. Caller must Close. Returns an error if no file was uploaded for this field.
func (*File) Present ¶ added in v0.4.0
Present reports whether a file part was actually uploaded for this field on the current action POST.
type Files ¶ added in v0.6.0
type Files struct {
// contains filtered or unexported fields
}
Files is a typed, request-scoped handle to ALL parts of a multi-file upload field (an <input type=file multiple>). Use it where via.File — which binds only the first part — would silently drop the rest:
type Page struct {
Photos via.Files `via:"photos"`
}
func (p *Page) Upload(ctx *via.Ctx) error {
for i, f := range p.Photos.All() {
if err := f.Save(fmt.Sprintf("/uploads/%d", i)); err != nil { return err }
}
return nil
}
Same lifecycle and wire-key rules as File.
func (*Files) All ¶ added in v0.6.0
All returns a File handle for each uploaded part, in form order.
type Group ¶ added in v0.2.3
type Group struct {
// contains filtered or unexported fields
}
Group bundles routes under a shared path prefix and (optionally) a shared middleware chain. Middleware registered with g.Use wraps every handler registered via g.HandleFunc / g.Handle / via.Mount[C](g, ...).
func (*Group) Handle ¶ added in v0.4.0
Handle registers a non-via http.Handler under the group prefix. Same pattern shape as HandleFunc.
func (*Group) HandleFunc ¶ added in v0.4.0
HandleFunc registers a non-via handler under the group prefix, wrapped in the group's middleware chain. The pattern follows the same shape as http.ServeMux — `"/users"` is GET-only by convention, `"POST /users"` registers POST. Without a method token, GET is assumed.
func (*Group) Use ¶ added in v0.2.3
func (g *Group) Use(mw ...Middleware)
Use installs middleware that wraps handlers registered through this group.
type Initializer ¶ added in v0.4.0
Initializer is the optional lifecycle hook that runs on the page-render request before View. Use it to seed reactive state from the request (cookies, query params), kick off OnInit-time fetches, or prepare any data View needs. A non-nil error is logged but does not abort the render.
The framework discovers OnInit via reflection on the method name — satisfying this interface is not required, but declaring it on the composition makes the hook self-documenting and surfaces it in Go tooling.
type KeyStore ¶ added in v0.5.0
type KeyStore interface {
// KeyFor returns the subject's key, creating one if absent. Used at Append.
KeyFor(ctx context.Context, subject string) ([]byte, error)
// Key returns the subject's key and ok=true, or ok=false if it never existed
// or was dropped. Used at decode — it MUST NOT create a key.
Key(ctx context.Context, subject string) (key []byte, ok bool, err error)
// DropKey permanently erases the subject's key (crypto-shred). Idempotent.
DropKey(ctx context.Context, subject string) error
}
KeyStore is the per-data-subject key custodian behind crypto-shred GDPR erasure. Each data subject (a user, a tenant) gets its own symmetric key; an event that carries PII is encrypted under its subject's key before it is appended to the durable log. Erasure is DropKey: once a subject's key is gone, every ciphertext for that subject — in the append-only log, in backups, in a Kafka topic that keeps records forever — is permanently unreadable, without rewriting any history.
Implementations must be safe for concurrent use. KeyFor is the create-or-get used at Append; Key is the lookup-only used at decode and MUST NOT recreate a dropped key (that would silently un-erase the subject); DropKey is idempotent.
func InMemoryKeyStore ¶ added in v0.5.0
func InMemoryKeyStore() KeyStore
InMemoryKeyStore returns a process-local KeyStore backed by a map. It is the reference implementation for tests and single-process apps; a clustered deployment supplies a shared KMS/Vault-backed KeyStore so every pod resolves and erases the same keys.
type LocalSignal ¶ added in v0.6.0
type LocalSignal struct {
// contains filtered or unexported fields
}
LocalSignal is a client-only reactive value — a Datastar `_`-prefixed signal that lives entirely in the browser, is never sent to the server, and needs no composition struct field or Mount binding. Use it for ephemeral UI state (menu open/closed, hover, active tab) that should react instantly without a server round-trip:
open := via.Local("open")
h.Div(open.Init(false),
h.Button(open.Toggle(), h.Text("menu")),
h.Nav(open.Show(), ...),
)
For state the server must see or persist, use a Signal or State* handle instead — a LocalSignal is intentionally invisible to actions.
func Local ¶ added in v0.6.0
func Local(name string) LocalSignal
Local returns a client-only signal handle named "_"+name. name must be a valid JS identifier fragment (letters, digits, underscores).
func (LocalSignal) Bind ¶ added in v0.6.0
func (l LocalSignal) Bind() h.H
Bind returns a two-way binding attribute for form inputs (data-bind="_name").
func (LocalSignal) Class ¶ added in v0.6.0
func (l LocalSignal) Class(name string) h.H
Class toggles the named CSS class by the signal's truthiness. See Signal.Class for the lower-case attribute-name caveat.
func (LocalSignal) Init ¶ added in v0.6.0
func (l LocalSignal) Init(v any) h.H
Init declares the signal with an initial value via Datastar's object form (data-signals="{_name:<json>}"). Place it once on a container element that wraps the uses of this signal.
func (LocalSignal) Ref ¶ added in v0.6.0
func (l LocalSignal) Ref() string
Ref returns the "$_name" expression for use in raw Datastar expressions.
func (LocalSignal) Show ¶ added in v0.6.0
func (l LocalSignal) Show() h.H
Show toggles the host element's display by the signal's truthiness.
func (LocalSignal) ShowUnless ¶ added in v0.6.0
func (l LocalSignal) ShowUnless() h.H
ShowUnless is the negation of LocalSignal.Show.
EXPERIMENTAL: a young convenience helper; may change before 1.0.
func (LocalSignal) Text ¶ added in v0.6.0
func (l LocalSignal) Text() h.H
Text binds the signal's value as the host element's text content.
func (LocalSignal) Toggle ¶ added in v0.6.0
func (l LocalSignal) Toggle() h.H
Toggle returns an on:click attribute that flips a boolean local signal — the canonical client-only toggle with no server round-trip.
type Logger ¶ added in v0.4.0
Logger receives log records produced by the via runtime. Implementations are free to forward to any logger of their choice — slog, zap, zerolog, a test buffer, /dev/null. The default logger writes to log.Printf with a "[level]" prefix.
Field pairs are appended after the message:
logger.Log(LogError, "action failed", "via_tab", id, "name", "Inc") → default output: [error] action failed via_tab=… name=Inc
Field values may be any type; the default formatter renders with %v.
func Log ¶ added in v0.4.0
Log returns the logger configured on the App that owns ctx, stamped with the current via_tab so every record is correlated to the tab that produced it. Falls back to the default logger if ctx is nil or otherwise has no App attached. Use it inside actions / OnInit / OnConnect to write app-level structured logs through the same pipe via uses for its own warnings:
via.Log(ctx).Log(via.LogInfo, "checkout", "user", id, "amount", n)
func SlogLogger ¶ added in v0.4.0
SlogLogger adapts a *slog.Logger to via's Logger. via's level maps onto slog's directly (Debug, Info, Warn, Error). Field pairs are passed through as slog attrs.
app := via.New(via.WithLogger(via.SlogLogger(slog.Default())))
type LoggerFunc ¶ added in v0.4.0
LoggerFunc adapts a function into a Logger.
type MapOps ¶ added in v0.4.0
type MapOps[K comparable, V any] struct { // contains filtered or unexported fields }
MapOps is the chain returned by Op(ctx) on every Map* reactive type; its map verbs route through the handle's Update path.
func (*MapOps[K, V]) Delete ¶ added in v0.4.0
func (o *MapOps[K, V]) Delete(k K)
Delete removes the entry at k. No-op if absent.
type Metrics ¶ added in v0.4.0
type Metrics interface {
Counter(name string, labels ...string)
Gauge(name string, value float64, labels ...string)
Histogram(name string, value float64, labels ...string)
}
Metrics is the optional integration seam for ops observability. via emits structured events at well-known names; the implementation routes them to whatever backend the operator picked (Prometheus, OTel, statsd, expvar, …). Install via WithMetrics.
The default implementation is [noopMetrics], which discards every event — apps that don't configure metrics pay no allocation cost.
Event catalogue (every name via emits; keep in sync with the call sites):
Actions & render:
- "via.action.total" counter, labels: method
- "via.action.latency" histogram (seconds), labels: method
- "via.render.total" counter, labels: route
SSE lifecycle:
- "via.sse.connect" counter — each successful handshake
- "via.sse.disconnect" counter, labels: reason ("client", "shutdown")
- "via.sse.recover" counter, labels: mode ("reload", "rebootstrap")
- "via.sse.resync" counter — a tab re-synced its signal state
Tab (Ctx) lifecycle:
- "via.ctx.live" gauge — current registered tab count
- "via.ctx.reap" counter, labels: reason ("ttl", "shutdown")
Session:
- "via.session.mismatch" counter — an action/SSE handshake's bound session no longer matched the request cookie (403); usually two co-located via apps clobbering one another's session cookie
Event-log projection (StateAppEvents projector), all labelled by key:
- "via.events.epoch_reset" counter — stream generation reset, re-folded
- "via.events.forward_incompatible" counter — record from a newer binary; key halted
- "via.events.erased" counter — crypto-shred-erased payload, skipped
- "via.events.undecodable" counter — poison record, skipped
- "via.events.compaction_reseed" counter — gap recovered from a bridging snapshot
- "via.events.compaction_gap_halt" counter — unbridgeable compacted gap; key halted
Fold-divergence canary:
- "via.fold.offset" gauge, labels: key — applied offset after each fold
- "via.fold.digest" gauge, labels: key, offset — projection digest at that offset
- "via.fold.divergence" counter, labels: key — WithFoldVerify saw an impure fold
Snapshot cold-start:
- "via.snapshot.unbridgeable" counter, labels: key — compacted snapshot, no migration; halted
- "via.snapshot.erasure_halt" counter, labels: key — compacted snapshot invalidated by erasure; halted
Backplane tailers (the shared changes and broadcast feeds, plus each StateAppEvents projector), labelled by feed — "changes", "broadcast", or "projector:<key>":
- "via.backplane.tailer_reconnect" counter — a tailer re-established its subscription after a transient disconnect or failed subscribe attempt (emitted once the fresh subscription is live)
- "via.backplane.tailer_up" gauge 0/1 — whether the tailer currently holds a live subscription
Side-effect consumers (OnEvent), all labelled by name, key:
- "via.consumer.forward_incompatible" counter — record from a newer binary
- "via.consumer.erased" counter — erased payload, skipped
- "via.consumer.undecodable" counter — poison record, skipped
- "via.consumer.error" counter — the consumer callback returned an error
Labels are passed as flat key,value pairs to keep the call site allocation-free in the noop path.
type Middleware ¶ added in v0.2.3
Middleware is the request-wrapping function shape used by App.Use. Each middleware receives the next handler in the chain and decides whether to invoke it, short-circuit (e.g. with a 401), or wrap the response writer before passing through. Registration order is outer-first: the first middleware passed to Use runs first per request.
Pre-built middleware lives in via/mw — RequestID, AccessLog, Recover, CSP, HSTS, RedirectHTTPS.
type Mountable ¶ added in v0.4.0
type Mountable interface {
// contains filtered or unexported methods
}
Mountable is the target of Mount. Implemented by *App (mounts at route on the app) and *Group (mounts under the group's prefix with the group's middleware applied to page render, action POST, and SSE handshake). The interface has only unexported methods so external types cannot implement it.
type NumOps ¶ added in v0.4.0
type NumOps[T Number] struct { // contains filtered or unexported fields }
NumOps is the chain returned by Op(ctx) on every Num* reactive type; its numeric verbs route through the handle's Update path.
func (*NumOps[T]) AtLeast ¶ added in v0.7.0
func (o *NumOps[T]) AtLeast(lo T)
AtLeast raises the value to lo when it is below; in-range values are untouched. After this call the value is at least lo.
func (*NumOps[T]) AtMost ¶ added in v0.7.0
func (o *NumOps[T]) AtMost(hi T)
AtMost lowers the value to hi when it is above; in-range values are untouched. After this call the value is at most hi.
func (*NumOps[T]) Clamp ¶ added in v0.7.0
func (o *NumOps[T]) Clamp(lo, hi T)
Clamp confines the value to [lo, hi]: values below lo are raised to lo, values above hi are lowered to hi, in-range values are untouched. Inverted bounds (lo > hi) are a programming mistake — Clamp panics rather than silently swapping or picking one bound.
type Number ¶ added in v0.4.0
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64
}
Number is the constraint for SignalNum / StateTabNum / StateSessNum / StateAppNum. Covers every Go-built-in integer and floating-point kind. Underlying-type approximation (~int etc.) lets users wrap these in named types (e.g. type UserID int) and still pick up the typed ops.
type Offset ¶ added in v0.5.0
type Offset uint64
Offset is an opaque, per-key, monotonically-INCREASING cursor. It is the resume primitive: a pod that committed Offset N resumes at "everything after N" and provably cannot miss a record. Treat it as OPAQUE — comparable and ordered WITHIN one key, never interchangeable across keys or backends, and not guaranteed gap-free in a real backend. Offset(0) means "before the first record"; Subscribe(from:0) replays all.
Monotone, NOT contiguous: a backend may assign a key's offsets from a sequence it shares across keys (e.g. a single NATS JetStream stream sequenced globally across subjects), so a key's records can skip numbers other keys took — 3, 7, 12 rather than 1, 2, 3. The projector treats such gaps as benign and folds every delivered record; it only suspects a lost prefix (and reseeds from, or halts behind, a snapshot) when a Compacted snapshot proves one. Implementations MUST keep offsets strictly increasing per key and stable across a resume; they need NOT make them dense.
type Option ¶ added in v0.2.0
type Option func(*config)
Option configures a via App.
func WithActionErrorHandler ¶ added in v0.4.0
WithActionErrorHandler replaces the default browser-alert with a custom callback for action errors and panics. The error from a panic is wrapped as fmt.Errorf("panic: %v", recovered).
func WithBackplane ¶ added in v0.5.0
WithBackplane wires the state backplane that makes app/session-scoped reactive state survive restarts and span a cluster. The default (no option, or a nil b) resolves internally to InMemory, so the Backplane interface is exercised on every single-pod run and there is no nil-special-case path. Wire it once at boot; it is never swapped at runtime.
EXPERIMENTAL: the clustered/distributed path is pre-GA. Single-pod use (the default InMemory backplane) is stable, but the Backplane interface and its cross-pod consistency semantics may change before 1.0 — 1.0 does not promise a distributed GA. Wire a custom backplane knowing the contract can shift.
func WithContextTTL ¶ added in v0.3.0
WithContextTTL sets how long a *stream-less* tab Ctx lingers before the idle sweep reclaims it. Default 15 minutes; a value <= 0 disables the sweep (contexts never expire).
It governs only ctxs with no open SSE stream — a page GET that never opened the stream, or the gap after a stream drops. A connected tab is kept alive for its stream's lifetime regardless of this value, so a short TTL can never reap a live tab.
func WithDescription ¶ added in v0.4.0
WithDescription sets the <meta name="description"> tag included in every rendered page. Search engines and link previews use it.
func WithFoldVerify ¶ added in v0.5.0
func WithFoldVerify() Option
WithFoldVerify turns on runtime fold-determinism checking for every StateAppEvents projector: each record is folded a SECOND time from the same accumulator and the two results compared. A mismatch means the reducer is impure (it read a clock, RNG, mutable global, or otherwise depends on something other than (acc, event)) — the runtime emits via.fold.divergence AND permanently REFUSES to compact that key, so a non-deterministic fold can never be crystallized into a durable-genesis snapshot (which no later re-fold could recover). It roughly doubles fold CPU, so it is opt-in — run it in dev/CI (and optionally a canary pod) to catch impurity before it reaches production compaction. Off by default.
The check uses reflect.DeepEqual on the two fold results, which can spuriously flag a PURE fold whose V contains uncomparable parts (func/chan fields, or NaN floats — for which a==a is false). That is fail-SAFE, not fail-open: the worst case is a needless via.fold.divergence and a refusal to compact, never a bad snapshot. Such a V is also not JSON-snapshottable, so it could not be compacted anyway.
func WithHTTPServer ¶ added in v0.4.0
WithHTTPServer hands the user the *http.Server before listening so non-default fields (TLSConfig, ConnState, …) can be set.
func WithIdleTimeout ¶ added in v0.4.0
WithIdleTimeout overrides the default 120 s idle-timeout. Affects the lifetime of HTTP/1.1 keep-alive connections; SSE streams are exempt.
func WithInsecureCookies ¶ added in v0.4.0
func WithInsecureCookies() Option
WithInsecureCookies clears the Secure flag so the session cookie rides a plain-http origin. The Secure default is the safe production posture (a framework aimed at internal tools should not ship a cookie that leaks on an http downgrade); reach for this only on a local http:// dev loop. Conflicts with WithSecureCookies.
func WithKeyStore ¶ added in v0.5.0
WithKeyStore enables per-data-subject encryption (crypto-shred GDPR erasure) for StateAppEvents. Events whose type implements DataSubject have their payload encrypted under the subject's key (from the KeyStore) before they are appended to the durable log, and App.EraseDataSubject drops a subject's key so every ciphertext for them becomes permanently unreadable — even in an append-only log or a backup — without rewriting history. Non-DataSubject events are unaffected (stored plaintext).
In a cluster every pod must share the SAME KeyStore (a KMS/Vault-backed impl), or a pod without a subject's key cannot decode that subject's events.
func WithLang ¶ added in v0.4.0
WithLang sets the <html lang="…"> attribute. Required for screen readers and language-aware browser features.
func WithLogLevel ¶ added in v0.2.0
WithLogLevel sets the minimum log severity.
func WithLogger ¶ added in v0.4.0
WithLogger replaces the default log.Printf-backed logger with a custom Logger (slog, zap, zerolog, a test buffer, …). All runtime warnings and errors flow through this callback as level + message + key/value pairs.
func WithMaxContexts ¶ added in v0.4.0
WithMaxContexts caps the number of concurrent live tabs. New page renders past the cap return 503 instead of registering a Ctx — a crude but effective floor against tab-spam DoS. Default 0 (no cap). Tune to (expected peak users × tabs per user × 2).
func WithMaxRequestBody ¶ added in v0.4.0
WithMaxRequestBody caps body bytes for action POSTs that ship as application/json (Datastar's default action payload). Default 1 MiB. File-upload actions use a multipart body and are governed by the separate WithMaxUploadSize knob, since file parts inflate body size beyond what a typed-signal JSON payload ever needs.
func WithMaxSessions ¶ added in v0.7.0
WithMaxSessions caps the number of concurrent live sessions. Once the cap is met, a request that would mint or adopt a NEW session is rejected with 503 instead of growing the session map — a crude floor against the cookieless-crawler flood that would otherwise OOM the pod. A client that already holds a session is unaffected. Default 0 (no cap). Tune to (expected peak users × 2).
func WithMaxUploadSize ¶ added in v0.4.0
WithMaxUploadSize caps total multipart body bytes for action POSTs that include file parts. Default 32 MiB. The cap applies to the wire body (all files + form fields combined); the per-file in-memory cap is the lower of this value and 32 MiB before parts spill to disk.
func WithMetrics ¶ added in v0.4.0
WithMetrics installs a Metrics backend that receives counter / gauge / histogram events for actions, renders, SSE connect/disconnect, and tab-count gauges. Default is a no-op backend, so configuring this is purely additive. See the Metrics godoc for the event catalogue.
func WithNotFound ¶ added in v0.4.0
WithNotFound replaces the default 404 page with a custom handler. The handler runs after the session middleware, so it can read the session and decide whether to redirect, render a "not found" composition, or short-circuit with an empty body.
func WithPlugins ¶ added in v0.2.0
WithPlugins registers plugins. They run Register at New time.
func WithReadHeaderTimeout ¶ added in v0.4.0
WithReadHeaderTimeout overrides the default 10 s read-header timeout.
func WithReadTimeout ¶ added in v0.4.0
WithReadTimeout sets http.Server.ReadTimeout. The SSE handler doesn't honor it (the stream is meant to be long-lived), but action POSTs do.
func WithReconcileInterval ¶ added in v0.5.0
WithReconcileInterval sets how often each pod re-pulls its value-shaped StateApp keys to the backplane Store HEAD. This periodic sweep makes the changes feed a pure latency optimization: a pod converges to shared state even when no Change hint reached it (a pod that joined after the write, a crash between the CAS and the hint append, or a silent Update). 0 disables the sweep (the changes feed alone then carries convergence). Default 5s.
func WithRequestTooLarge ¶ added in v0.6.0
WithRequestTooLarge sets the handler invoked when an action POST exceeds the body cap (WithMaxRequestBody / WithMaxUploadSize) — the limit trips in MaxBytesReader before any action handler runs, so this is the only place to turn the default bare "request too large" 413 into a friendly response (e.g. redirect a too-large file upload back to its form with a flash message). h receives the raw request; without this option the framework writes a plain 413. This covers action POSTs only: an oversize SSE-close body always gets the bare 413, since that payload is Datastar's internal reconnect frame, not a user-facing submit.
func WithSSEHeartbeat ¶ added in v0.3.0
WithSSEHeartbeat sets the SSE keepalive cadence. Default 25s.
A connected tab is kept alive for its stream's lifetime regardless of this value — the keepalive's only job is to detect a silently-dropped (half- open) client via a failed write, which then reaps the Ctx. Because that failed write is the sole in-band detector of a vanished client, a value <= 0 does NOT disable the keepalive; it floors to a safe default (25s). Slow it down if you must, but it can't be silenced.
func WithSSEWriteTimeout ¶ added in v0.4.0
WithSSEWriteTimeout caps how long a single SSE drain may block on the underlying connection before the stream is torn down. Bounds the blast radius of slow / stalled clients (without this, a wedged TCP peer pins the server goroutine for the lifetime of the tab). Default 10 seconds.
Keep it nonzero in production: a failed keepalive write is the only in-band detector of a vanished (half-open) client, and a connected Ctx is never TTL-swept — so setting 0 lets a half-open peer pin its Ctx and goroutine until the OS TCP keepalive fires (or the process exits).
func WithSecureCookies ¶ added in v0.3.0
func WithSecureCookies() Option
WithSecureCookies marks the session cookie Secure. This is the default; the option remains for explicit intent and conflicts with WithInsecureCookies.
func WithSessionCookieName ¶ added in v0.6.0
WithSessionCookieName overrides the default "via_session" cookie name. Two via apps on the same host (different ports) otherwise share — and clobber — one cookie, since the port is not part of a cookie's scope. Give co-located apps distinct names to keep their sessions independent.
func WithSessionTTL ¶ added in v0.2.3
WithSessionTTL sets the per-session expiry. Default 30 minutes.
func WithShutdownTimeout ¶ added in v0.2.0
WithShutdownTimeout sets the graceful shutdown timeout.
func WithSnapshotInterval ¶ added in v0.5.0
WithSnapshotInterval sets how many folds a StateAppEvents projector applies before persisting a fold snapshot, so a cold start replays only the tail after the snapshot's offset instead of re-folding the whole log. 0 (or less) disables snapshot writes. The snapshot is a disposable cache — never required for correctness; a missing or stale-codec snapshot just re-folds from genesis. Default 64.
func WithStrictDecode ¶ added in v0.7.0
func WithStrictDecode() Option
WithStrictDecode rejects a client signal value that cannot be represented in its Signal[T] type — a number that overflows the target int/uint/float width, or a value whose JSON shape doesn't match the field — instead of silently truncating it (the best-effort default). The offending action surfaces an error and its handler does not run, so corrupt input can't reach server state. Off by default; turn it on when client input is untrusted and a lossy decode must fail loud rather than silently clamp.
EXPERIMENTAL: a diagnostic knob; its name or default may change before 1.0.
func WithVerboseErrors ¶ added in v0.7.0
func WithVerboseErrors() Option
WithVerboseErrors surfaces the real error message of a recovered action panic to the browser instead of the generic "Something went wrong". The typed error already reaches a custom WithActionErrorHandler and the server log regardless; this only controls what the DEFAULT client notification shows. Off by default — leaking raw panic text to clients is an information-disclosure risk. Turn it on in development for faster feedback.
EXPERIMENTAL: a diagnostic knob; its name or default may change before 1.0.
func WithWriteTimeout ¶ added in v0.4.0
WithWriteTimeout sets http.Server.WriteTimeout. Be cautious: SSE streams are long-lived, so a non-zero WriteTimeout can cut them off mid-stream. Default 0 (no timeout) is safer for SSE-heavy apps.
func WithoutDevChecks ¶ added in v0.7.0
func WithoutDevChecks() Option
WithoutDevChecks disables via's by-default runtime binding check. That check runs once per composition descriptor (the cost amortizes to ~zero across renders): after OnInit it verifies no bound state handle was orphaned by reassigning a child composition (p.Child = &T{...}), which silently orphans the runtime's by-address binding and leaves the page rendering once then going dead. It's on by default because that footgun is silent and expensive to debug; opt out only if it ever false-positives in your build.
EXPERIMENTAL: a diagnostic knob; its name or default may change before 1.0.
func WithoutHealthEndpoints ¶ added in v0.7.0
func WithoutHealthEndpoints() Option
WithoutHealthEndpoints disables via's built-in GET /livez, /healthz, and /readyz probes. By default they are served before the session and middleware chain (so a frequent probe never mints a session or logs a request): /livez and /healthz report 200 while the process is up; /readyz reports 503 once Shutdown begins draining. Opt out when the app needs to own those paths.
func WithoutSSEReconnect ¶ added in v0.7.0
func WithoutSSEReconnect() Option
WithoutSSEReconnect removes the small client-side reconnect manager via injects into every page. By default that manager watches Datastar's fetch lifecycle: it shows a "Reconnecting…" banner while the SSE stream is retrying and, once Datastar's retries are exhausted (a graceful-deploy clean close or a persistent failure that would otherwise leave the tab silently frozen), reloads the page to re-bootstrap a fresh stream and session — bounded by a reload-loop guard. Opt out to supply your own reconnect handling.
type Patch ¶ added in v0.4.0
type Patch struct {
// contains filtered or unexported fields
}
Patch groups the low-level wire-push primitives — push a signal value for a key not bound to a typed Signal[T] field, or morph an arbitrary element fragment into the live DOM. Reach for these only when the typed path (Signal.Write, View re-render) doesn't fit:
ctx.Patch().Signal("_picoTheme", "purple") // ad-hoc client signal
ctx.Patch().Signals(map[string]any{"a": 1, "b": 2}) // batched merge
ctx.Patch().Element(h.Div(h.ID("toast"), ...)) // single morph
ctx.Patch().Elements(div1, div2) // variadic morph batch
The Patch handle is allocated eagerly in newCtx; ctx.Patch() is a plain field load with no allocation. Mirrors how *CtxR is cached.
func (*Patch) Element ¶ added in v0.4.0
Element pushes a single h.H tree to the client as an element patch at the next flush. The element should carry h.ID("…") so the client knows where to morph it. Nil element is a no-op.
func (*Patch) Elements ¶ added in v0.4.0
Elements pushes one or more h.H trees to the client as element patches at the next flush. Useful for action-driven, targeted DOM updates that bypass the full view re-render. Each element should carry h.ID("…") so the client knows where to morph it.
Multiple Elements calls within the same action — and any view re-render queued by State mutations earlier in the same action — are concatenated, not overwritten. The browser's morph applies each element patch independently by ID, so a State write followed by a targeted Elements call both reach the DOM in one SSE frame. Nil elements within the variadic list are skipped.
func (*Patch) Signal ¶ added in v0.4.0
Signal queues a single signal update keyed by name. Plugins use it to push values to client-only signals they own (e.g. picocss's "_picoTheme") without going through a typed Signal[T] handle. Multiple Signal/Signals calls within the same flush window are merged — last write wins per key. Empty key is a no-op.
type Plugin ¶
type Plugin interface {
Register(*App)
}
Plugin extends the App at registration time.
EXPERIMENTAL: the plugin system (this interface and the bundled picocss / echarts / maplibre packages) is young and may change before 1.0.
type Record ¶ added in v0.5.0
Record is one delivered EventLog entry. The runtime, not the backend, interprets Data; the backend only moves bytes and assigns Offset.
type Rev ¶ added in v0.5.0
type Rev uint64
Rev is the Store cell's CAS version, DISTINCT from Offset (Store and EventLog keep independent counters). Rev(0) means "the cell has never been written".
type RouteInfo ¶ added in v0.4.0
type RouteInfo struct {
Pattern string // method-and-pattern, e.g. "GET /counter/{id}"
RegisteredBy string // who claimed it: "Mount[Counter]", "HandleFunc", …
}
RouteInfo is one entry in App.Routes().
type Session ¶ added in v0.4.0
type Session struct {
// contains filtered or unexported fields
}
Session is the per-browser session value bag. Survives tab close; expires per WithSessionTTL.
A Session obtained via Ctx.Session marks the page dirty + fans out to subscribed tabs on writes; one obtained via RequestSession (in a middleware, before a Ctx exists) is cookie-only and does not trigger re-render.
All value access is typed and lives in the via/sess subpackage — sess.Get[T] / sess.Put[T] / sess.Clear[T]. Session itself only exposes Session.Rotate.
func RequestSession ¶ added in v0.4.0
RequestSession returns the Session cookie-resolved off r, or a detached Session (reads/writes no-op) if the request carries no via session yet. Use this from middleware that needs to read or write session state before any composition is rendered.
Writes performed via the returned Session do not trigger a tab re-render — there is no Ctx attached. Use Ctx.Session from inside actions / handlers when re-render fan-out is required.
func (*Session) Rotate ¶ added in v0.4.0
Rotate issues a fresh session id, copies the existing session's data into it, and points the bound Ctx + the cookie on the in-flight response at the new session. Returns the new session id, or "" if rotation could not be performed (no bound Ctx, no Writer, no App).
Use after authentication state changes (login, privilege elevation, password reset) so any captured pre-auth session id can no longer impersonate the user.
type Signal ¶ added in v0.2.0
type Signal[T any] struct { // contains filtered or unexported fields }
Signal is a typed reactive value mirrored to the browser. The value lives inside the composition struct; Read/Write go through the bound *Ctx so changes are tracked and propagated over SSE.
type Counter struct {
Step via.Signal[int] `via:"step,init=1"`
}
c.Step.Read(ctx) // returns int
c.Step.Write(ctx, 5) // marks dirty, browser updates next flush
c.Step.Bind() // <input> two-way bind: data-bind="step"
c.Step.Text() // data-text="$step" attribute (attach to any element)
c.Step.TextSpan() // <span data-text="$step"></span>
Untyped, untagged Signal[T] fields use the lower-cased field name as the wire key. Tag form: `via:"name,init=value"`; either part is optional.
func (*Signal[T]) Attr ¶ added in v0.4.0
Attr returns a data-attr:<name> attribute that mirrors this signal's truthiness onto the host element's HTML attribute. Truthy → attribute present (boolean form, e.g. `disabled`); falsy → attribute absent. For string-valued attributes, the attribute value tracks the signal.
h.Button(c.Saving.Attr("disabled"), h.Text("Save"))
h.A(c.Target.Attr("href"), h.Text("Open"))
func (*Signal[T]) Bind ¶ added in v0.4.0
Bind returns a two-way binding attribute. Use on form inputs.
func (*Signal[T]) Class ¶ added in v0.6.0
Class toggles the named CSS class on the host element by this signal's truthiness, emitting Datastar's data-class:<name> attribute.
name must be lower-case (or kebab-case). HTML attribute names are folded to lower-case by the browser parser, so a mixed-case name like "myThing" resolves to the class "mything" at runtime — pass "my-thing" if you need a hyphen. For a camelCase class name use Datastar's object form via h.DataClass with an explicit expression instead.
func (*Signal[T]) Key ¶ added in v0.4.0
Key returns the wire key (qualified field path). Useful in tests.
func (*Signal[T]) Read ¶ added in v0.4.0
func (s *Signal[T]) Read(_ readCtx) T
Read returns the current value. The ctx is unused today but kept so every reactive-handle Read has the same shape (and so future tab- scoped reads can move into the runtime without an API break). Accepts either *Ctx (action handlers) or *CtxR (View).
func (*Signal[T]) Show ¶ added in v0.4.0
Show returns a data-show attribute that toggles display by truthiness.
func (*Signal[T]) ShowUnless ¶ added in v0.6.0
ShowUnless is the negation of Signal.Show: the element is hidden while the signal is truthy and shown while falsy. Saves hand-writing the "!$key" expression (and re-juggling the $ prefix the typed helpers exist to hide).
EXPERIMENTAL: a young convenience helper; may change before 1.0.
func (*Signal[T]) Style ¶ added in v0.4.0
Style returns a data-style:<prop> attribute that drives an inline CSS property from this signal's stringified value. Pairs naturally with `Signal[string]` carrying a colour, length, etc.
h.Div(c.Hue.Style("background-color"))
func (*Signal[T]) Text ¶ added in v0.4.0
Text returns a reactive `data-text="$key"` attribute that binds this signal's value as the text content of whatever element it is attached to. For a standalone reactive span use Signal.TextSpan.
func (*Signal[T]) TextSpan ¶ added in v0.6.0
TextSpan wraps Signal.Text in its own span: <span data-text="$key"></span>. Use it where no host element is available to carry the binding.
EXPERIMENTAL: a young convenience helper; may change before 1.0.
func (*Signal[T]) Update ¶ added in v0.4.0
Update atomically applies fn to the current value. fn receives the current T and returns (new T, error). On non-nil error the value is unchanged and the error is returned. Saves a Read/Write pair on transform-the-current-value patterns and is the only mutation path that lets a user reject a write (validation, conflict detection).
Panics on nil ctx: without one, the write cannot reach the browser, so silently succeeding would desync server state from the client. From a raw goroutine, pass the bound *Ctx and call ctx.SyncNow() at a flush boundary.
func (*Signal[T]) Write ¶ added in v0.4.0
Write stores a new value and marks the signal dirty so the next flush patches it to the browser. From inside an action method or a via.Stream callback, the flush is automatic. From a raw goroutine you started yourself, call ctx.SyncNow() at a coalescing boundary — the dirty bit alone won't reach the browser without a flush.
Sugar over Update(ctx, func(T) (T, error) { return v, nil }) — the non-fallible path for "replace with a constant." Panics on nil ctx for the same reason as Update.
type SignalBool ¶ added in v0.4.0
SignalBool is the bool-specialized Signal — client-mirrored reactive bool with a typed Op(ctx) chain.
func (*SignalBool) Op ¶ added in v0.4.0
func (s *SignalBool) Op(ctx *Ctx) *BoolOps
Op returns a bool chain bound to ctx.
type SignalMap ¶ added in v0.4.0
type SignalMap[K comparable, V any] struct{ Signal[map[K]V] }
SignalMap is the map-specialized Signal.
type SignalNum ¶ added in v0.4.0
SignalNum is the numeric-specialized Signal — same client-mirrored reactive value as Signal[T], with a typed Op(ctx) chain.
type SignalSlice ¶ added in v0.4.0
SignalSlice is the slice-specialized Signal.
func (*SignalSlice[T]) Op ¶ added in v0.4.0
func (s *SignalSlice[T]) Op(ctx *Ctx) *SliceOps[T]
Op returns a slice chain bound to ctx.
type SliceOps ¶ added in v0.4.0
type SliceOps[T any] struct { // contains filtered or unexported fields }
SliceOps is the chain returned by Op(ctx) on every Slice* reactive type; its slice verbs route through the handle's Update path.
func (*SliceOps[T]) Append ¶ added in v0.4.0
func (o *SliceOps[T]) Append(v T)
Append adds v to the end.
func (*SliceOps[T]) Drop ¶ added in v0.4.0
Drop discards the first n elements. n <= 0 is a no-op; n >= len clears.
func (*SliceOps[T]) Empty ¶ added in v0.4.0
func (o *SliceOps[T]) Empty()
Empty replaces the value with nil (zero-length slice).
func (*SliceOps[T]) Filter ¶ added in v0.4.0
Filter keeps only elements for which pred returns true. Allocates a new slice so the result doesn't alias the input. Nil pred is a no-op.
func (*SliceOps[T]) Pop ¶ added in v0.4.0
func (o *SliceOps[T]) Pop()
Pop removes the last element. No-op on empty.
func (*SliceOps[T]) Prepend ¶ added in v0.4.0
func (o *SliceOps[T]) Prepend(v T)
Prepend adds v to the front. Allocates a new slice — in-place prepend isn't possible without reallocating.
type StateApp ¶ added in v0.4.0
type StateApp[T any] struct { // contains filtered or unexported fields }
StateApp is an app-scoped reactive value: shared across every session, every tab — and, with a clustered backplane, across every pod. Use sparingly (no tenant isolation).
type Profile struct {
Hits via.StateApp[int]
}
The handle holds only the wire key; the value lives in the backplane Store cell val:<key> (the single source of truth), cached per-pod in an L1 cell populated at Mount time. T must be JSON-serializable (the Store moves bytes).
func (*StateApp[T]) Key ¶ added in v0.4.0
Key returns the wire key (lowercase field name unless overridden by tag).
func (*StateApp[T]) Read ¶ added in v0.4.0
func (a *StateApp[T]) Read(rc readCtx) T
Read returns the current app value, or the zero value of T if unset. A Read during View execution subscribes the ctx so a subsequent Update on the same key (from any pod) fans out to it. O(1): hits the per-pod L1 cache, never the backplane. Accepts either *Ctx (action handlers) or *CtxR (View).
func (*StateApp[T]) Text ¶ added in v0.4.0
Text returns a static text node carrying the current value. Accepts either *Ctx (action handlers) or *CtxR (View).
func (*StateApp[T]) Update ¶ added in v0.4.0
Update atomically applies fn to the current app value. fn receives the current T and returns (new T, error). On non-nil error from fn the value is unchanged, no broadcast fires, and the error is returned. On success this tab re-renders and every other live tab — on this pod and, via the changes feed, on every other pod — subscribed to this key fans out a re-render.
The backplane Store cell val:<key> is the source of truth: Update runs a compare-and-swap retry loop against it, so concurrent Updates from different ctxs (or pods) cannot lose increments — the loser observes ErrCASConflict and re-runs fn on the reloaded value. Write is intentionally absent: a blind write on shared state is almost always a read-modify-write race in disguise — model the assignment as an Update whose fn ignores the old value if you truly mean it.
Panics on nil ctx: without one no broadcast can fan out, so silently succeeding would desync server state from every live tab.
type StateAppBool ¶ added in v0.4.0
StateAppBool is the bool-specialized StateApp.
func (*StateAppBool) Op ¶ added in v0.4.0
func (a *StateAppBool) Op(ctx *Ctx) *BoolOps
Op returns a bool chain bound to ctx.
type StateAppEvents ¶ added in v0.5.0
type StateAppEvents[E EventReducer[E, V], V any] struct { // contains filtered or unexported fields }
StateAppEvents is an app-scoped, event-sourced reactive value: the value is the fold of an append-only event log shared across every session and tab. Unlike StateApp[T] (which CAS-writes a single value), the projected value is derived purely from the log via E's Fold.
type Feed struct {
Posts via.StateAppEvents[PostEvent, []Post]
}
The zero value is usable: declare the field, no init. With a nil backplane it degrades to today's single-pod in-process behavior, no API difference.
Mutation grammar: StateAppEvents uses DIRECT verbs — l.Append(ctx, ev) and l.Read(ctx) — not the l.Op(ctx).Verb() grammar that the collection shapes (StateAppSlice/StateAppMap) use. That is deliberate, not an oversight: those shapes expose MANY mutators (Append/Insert/Delete/Set/…) and route them through a single Op(ctx) so the ctx is captured once; StateAppEvents has exactly ONE mutator (Append), so a direct ctx-first method is both simpler and consistent with its true peer, StateApp.Update(ctx, …), which is also a direct ctx-first verb. One verb → one method.
The handle holds only the wire key and the bound app; the log itself lives in the backplane owned by the via runtime, populated at Mount time.
func (*StateAppEvents[E, V]) Append ¶ added in v0.5.0
func (l *StateAppEvents[E, V]) Append(ctx *Ctx, ev E) (Offset, error)
Append commits ONE immutable event to the EventLog. Unlike StateApp.Update there is no read-modify-write and no old value: you describe WHAT HAPPENED, the fold derives the new value. Concurrent Appends never conflict (the EventLog orders them).
Append does NOT fold and does NOT render: the per-(pod,key) projector is the SOLE fold path on every pod incl. the writer (T1-SRE-2). The writer's own View updates when ITS projector folds this offset (one in-process hop), so cross-tab read-your-write is eventual. The returned offset is the commit position.
Panics on nil ctx, exactly like StateApp.Update — the ctx is the AUTHORIZATION gate (Append is reachable only from a via_tab + session-gated action ctx), and the projector, not the ctx, drives the re-render. A nil ctx means the call did not come from a legitimate tab action.
Error surface: returns a non-nil error only if the event cannot be encoded (a json.Marshal failure on ev — a programming error, surfaced rather than panicked) or the backplane rejects the append (e.g. ErrClosed during Shutdown). It returns (0, nil) — a deliberate no-op — before Mount when no backplane is bound, parity with StateApp's pre-Mount guard. Most call sites can ignore the error (as the chat example does); handle it where a failed append must be surfaced to the user (e.g. a form that should report "not saved").
func (*StateAppEvents[E, V]) Key ¶ added in v0.5.0
func (l *StateAppEvents[E, V]) Key() string
Key returns the wire key (lowercase field name unless overridden by `via:` tag).
func (*StateAppEvents[E, V]) OnEvent ¶ added in v0.5.0
func (l *StateAppEvents[E, V]) OnEvent(name string, fn func(ctx context.Context, ev E, off Offset) error, opts ...ConsumerOption)
OnEvent registers a named, offset-tracked, side-effecting consumer over this key's event log (send an email on ticket-closed, charge a card). Side effects do NOT live in Fold (Fold must stay pure) — they live here, in a separate tailer whose committed offset is durable in the Store cell "consumer:<name>:<wireKey>" and advanced ONLY after the handler returns nil.
A restart resumes from the committed offset; an event whose effect already ran is skipped. Delivery is at-least-once (a crash between effect and commit, or two pods both running the consumer, re-runs the handler), so a handler that must be exactly-once carries an idempotency key derived from off (e.g. a Stripe idempotency-key = wireKey+":"+off):
func (t *Tickets) OnInit(ctx *via.Ctx) {
t.Events.OnEvent("notify", func(ctx context.Context, ev TicketEvent, off via.Offset) error {
if ev.Kind != Closed {
return nil // not interested → committed, advance past it
}
// idempotency key = key+offset: a redelivery (crash/two pods) is a no-op
return mailer.Send(ctx, ev.Email, "Closed", mail.IdempotencyKey(t.Events.Key()+":"+strconv.FormatUint(uint64(off),10)))
})
}
Error surface, by outcome:
- handler returns nil → commit + advance (effect ran exactly once on this pod)
- handler returns an error → DO NOT advance; re-subscribe from committed and retry head-of-line (preserves order) with exponential backoff + jitter; via.consumer.error each attempt; via.consumer.stuck gauge tracks the attempt count so a floor-pin is observable. With WithMaxAttempts(n>0) the record is treated as poison after n attempts (dead-lettered or dropped) and the consumer advances; the DEFAULT (maxAttempts 0) blocks forever, never dropping a side effect.
- undecodable record → skip + advance (drop-on-undecodable, like the fold); via.consumer.undecodable.
- forward-incompatible → BLOCK (do not advance), so a rolled-back deploy never silently skips an event it cannot yet understand; via.consumer.forward_incompatible. NEVER poisoned regardless of WithMaxAttempts.
A registered consumer also pins the Compactor floor to its committed offset, so compaction never discards an event it has not yet processed.
The handler receives a context.Context scoping the delivery (cancelled when the consumer stops delivering), NOT a via *Ctx — a background tailer fires on records from any pod, with no originating tab/session. Registration is idempotent: the consumer starts once per (name,key) however many times OnEvent is called. No-op before Mount (the handle isn't bound yet).
func (*StateAppEvents[E, V]) Read ¶ added in v0.5.0
func (l *StateAppEvents[E, V]) Read(rc readCtx) V
Read returns the current PROJECTED value: the fold of every event up to this pod's locally-applied offset, seeded by the Go zero of V. A Read during View execution subscribes the ctx via trackRead — IDENTICAL to StateApp.Read — so a later Append (folded by the projector) fans a re-render out to this tab. O(1): returns the cached projection; never re-folds from genesis. Accepts *Ctx or *CtxR.
func (*StateAppEvents[E, V]) Text ¶ added in v0.5.0
func (l *StateAppEvents[E, V]) Text(rc readCtx) h.H
Text returns the projected value as a text node. Sibling of StateApp.Text. Accepts either *Ctx (action handlers) or *CtxR (View).
type StateAppMap ¶ added in v0.4.0
type StateAppMap[K comparable, V any] struct{ StateApp[map[K]V] }
StateAppMap is the map-specialized StateApp.
func (*StateAppMap[K, V]) Op ¶ added in v0.4.0
func (a *StateAppMap[K, V]) Op(ctx *Ctx) *MapOps[K, V]
Op returns a map chain bound to ctx.
type StateAppNum ¶ added in v0.4.0
StateAppNum is the numeric-specialized StateApp.
func (*StateAppNum[T]) Op ¶ added in v0.4.0
func (a *StateAppNum[T]) Op(ctx *Ctx) *NumOps[T]
Op returns a numeric chain bound to ctx.
type StateAppSlice ¶ added in v0.4.0
StateAppSlice is the slice-specialized StateApp.
func (*StateAppSlice[T]) Op ¶ added in v0.4.0
func (a *StateAppSlice[T]) Op(ctx *Ctx) *SliceOps[T]
Op returns a slice chain bound to ctx.
type StateAppStr ¶ added in v0.4.0
StateAppStr is the string-specialized StateApp.
func (*StateAppStr) Op ¶ added in v0.4.0
func (a *StateAppStr) Op(ctx *Ctx) *StrOps
Op returns a string chain bound to ctx.
type StateSess ¶ added in v0.4.0
type StateSess[T any] struct { // contains filtered or unexported fields }
StateSess is a session-scoped reactive value: shared across every tab opened from the same browser session, expires per via.WithSessionTTL.
type Profile struct {
Theme via.StateSess[string]
}
The handle holds only the wire key; the value lives in the backplane Store cell val:s:<sid>:<key> (the source of truth, so a session spans pods), cached per-pod in the session's data. T must be JSON-serializable (the Store moves bytes).
func (*StateSess[T]) Key ¶ added in v0.4.0
Key returns the wire key (lowercase field name unless overridden by tag).
func (*StateSess[T]) Read ¶ added in v0.4.0
func (s *StateSess[T]) Read(rc readCtx) T
Read returns the current session value, or the zero value of T if unset. A Read that happens during View execution subscribes the ctx so a subsequent Update on the same key fans out to it. Accepts either *Ctx (action handlers) or *CtxR (View).
func (*StateSess[T]) Text ¶ added in v0.4.0
Text returns a static text node carrying the current value. Accepts either *Ctx (action handlers) or *CtxR (View).
func (*StateSess[T]) Update ¶ added in v0.4.0
Update atomically applies fn to the current session value. fn receives the current T and returns (new T, error). On non-nil error the store is unchanged, no broadcast fires, and the error is returned. On success the current tab re-renders and every other live tab on the same session subscribed to this key fans out a re-render. The load → fn → store sequence runs under a per-key mutex so concurrent Update calls from different tabs on the same session cannot lose updates. Write is intentionally absent on session-scoped handles: a blind write across a user's open tabs is almost always a read-modify-write race in disguise — model the assignment as an Update whose fn ignores the old value if you truly mean it.
Panics on nil ctx: without one no broadcast can fan out, so silently succeeding would desync server state from every live tab.
type StateSessBool ¶ added in v0.4.0
StateSessBool is the bool-specialized StateSess.
func (*StateSessBool) Op ¶ added in v0.4.0
func (s *StateSessBool) Op(ctx *Ctx) *BoolOps
Op returns a bool chain bound to ctx.
type StateSessMap ¶ added in v0.4.0
type StateSessMap[K comparable, V any] struct{ StateSess[map[K]V] }
StateSessMap is the map-specialized StateSess.
func (*StateSessMap[K, V]) Op ¶ added in v0.4.0
func (s *StateSessMap[K, V]) Op(ctx *Ctx) *MapOps[K, V]
Op returns a map chain bound to ctx.
type StateSessNum ¶ added in v0.4.0
StateSessNum is the numeric-specialized StateSess.
func (*StateSessNum[T]) Op ¶ added in v0.4.0
func (s *StateSessNum[T]) Op(ctx *Ctx) *NumOps[T]
Op returns a numeric chain bound to ctx.
type StateSessSlice ¶ added in v0.4.0
StateSessSlice is the slice-specialized StateSess.
func (*StateSessSlice[T]) Op ¶ added in v0.4.0
func (s *StateSessSlice[T]) Op(ctx *Ctx) *SliceOps[T]
Op returns a slice chain bound to ctx.
type StateSessStr ¶ added in v0.4.0
StateSessStr is the string-specialized StateSess.
func (*StateSessStr) Op ¶ added in v0.4.0
func (s *StateSessStr) Op(ctx *Ctx) *StrOps
Op returns a string chain bound to ctx.
type StateTab ¶ added in v0.4.0
type StateTab[T any] struct { // contains filtered or unexported fields }
StateTab is a typed, server-only reactive value. Mutations trigger a view re-render and SSE patch. Tab-scoped: each browser tab has its own value.
For session-scoped or app-scoped state use StateSess[T] / StateApp[T].
type Counter struct {
Hits via.StateTab[int]
Filter via.StateTab[string] `via:"filter,init=all"`
}
c.Hits.Read(ctx) // returns int
c.Hits.Write(ctx, 0) // direct write
c.Hits.Update(ctx, func(n int) (int, error) { return n + 1, nil}) // numeric delta
The optional `via:"name,init=value"` tag mirrors Signal[T]: either part is optional, and init=… is decoded into the field at bind time.
func (*StateTab[T]) Read ¶ added in v0.4.0
func (s *StateTab[T]) Read(_ readCtx) T
Read returns the current value. The ctx is unused today but kept so StateTab[T] mirrors Signal[T]'s shape (and so future tab-scoped reads can move into the runtime without an API break). Accepts either *Ctx (action handlers) or *CtxR (View).
func (*StateTab[T]) Text ¶ added in v0.4.0
Text returns a static text node carrying the current value. Re-renders happen as part of the view fragment, not via a client signal. Mirrors StateSess/StateApp.Text so every reactive-value Text(ctx) reads the same way; the ctx is unused on StateTab (the value lives on the struct) and accepted only for signature parity. Accepts either *Ctx (action handlers) or *CtxR (View).
func (*StateTab[T]) Update ¶ added in v0.4.0
Update atomically applies fn to the current value. fn receives the current T and returns (new T, error). On non-nil error the value is unchanged and the error is returned. Saves a Read/Write pair on common increment/transform patterns and is the only mutation path that lets a user reject a write (validation, conflict detection):
err := c.Hits.Update(ctx, func(n int) (int, error) {
if n >= max { return 0, errBudget }
return n + 1, nil
})
Panics on nil ctx: without one the next flush cannot re-render, so silently succeeding would desync server state from the client.
func (*StateTab[T]) Write ¶ added in v0.4.0
Write stores a new value and marks the composition dirty so the next flush re-renders the view fragment. From inside an action method or a via.Stream callback, the flush is automatic. From a raw goroutine you started yourself, call ctx.SyncNow() at a coalescing boundary — the dirty bit alone won't reach the browser without a flush.
Sugar over Update(ctx, func(T) (T, error) { return v, nil }) — the non-fallible path for "replace with a constant." Panics on nil ctx for the same reason as Update.
type StateTabBool ¶ added in v0.4.0
StateTabBool is the bool-specialized StateTab.
func (*StateTabBool) Op ¶ added in v0.4.0
func (s *StateTabBool) Op(ctx *Ctx) *BoolOps
Op returns a bool chain bound to ctx.
type StateTabMap ¶ added in v0.4.0
type StateTabMap[K comparable, V any] struct{ StateTab[map[K]V] }
StateTabMap is the map-specialized StateTab.
func (*StateTabMap[K, V]) Op ¶ added in v0.4.0
func (s *StateTabMap[K, V]) Op(ctx *Ctx) *MapOps[K, V]
Op returns a map chain bound to ctx.
type StateTabNum ¶ added in v0.4.0
StateTabNum is the numeric-specialized StateTab.
func (*StateTabNum[T]) Op ¶ added in v0.4.0
func (s *StateTabNum[T]) Op(ctx *Ctx) *NumOps[T]
Op returns a numeric chain bound to ctx.
type StateTabSlice ¶ added in v0.4.0
StateTabSlice is the slice-specialized StateTab.
func (*StateTabSlice[T]) Op ¶ added in v0.4.0
func (s *StateTabSlice[T]) Op(ctx *Ctx) *SliceOps[T]
Op returns a slice chain bound to ctx.
type StateTabStr ¶ added in v0.4.0
StateTabStr is the string-specialized StateTab.
func (*StateTabStr) Op ¶ added in v0.4.0
func (s *StateTabStr) Op(ctx *Ctx) *StrOps
Op returns a string chain bound to ctx.
type Store ¶ added in v0.5.0
type Store interface {
// LoadSnapshot returns the stored bytes for key and its revision, or
// ok=false if the key was never written.
LoadSnapshot(ctx context.Context, key string) (data []byte, rev Rev, ok bool, err error)
// CAS stores data for key IFF the current revision == expectedRev (Rev(0)
// means "must not exist yet"). Returns the NEW revision, or ErrCASConflict
// if the current rev moved — the caller reloads and retries.
CAS(ctx context.Context, key string, expectedRev Rev, data []byte) (newRev Rev, err error)
}
Store is the durable per-key current-value cell with compare-and-swap.
type StrOps ¶ added in v0.4.0
type StrOps struct {
// contains filtered or unexported fields
}
StrOps is the chain returned by Op(ctx) on every Str* reactive type.
type Ticker ¶ added in v0.4.0
type Ticker struct {
// contains filtered or unexported fields
}
Ticker is the handle returned by Stream. It lets the caller pause, resume, or change the cadence of the running ticker. The underlying goroutine stops automatically when ctx is disposed; calling Pause / Resume / SetInterval on a stopped ticker is a no-op.
func Stream ¶ added in v0.4.0
Stream runs fn on a ticker until ctx is disposed. Use it in OnConnect to drive periodic UI updates without managing a goroutine and ticker by hand:
func (p *Page) OnConnect(ctx *via.Ctx) error {
via.Stream(ctx, time.Second, func(ctx *via.Ctx, t time.Time) {
p.Now.Write(ctx, t.Format("15:04:05"))
})
return nil
}
fn runs on the same goroutine for every tick; it must return promptly. Long work should be offloaded with its own goroutine that observes ctx.Done(). After fn returns, dirty signals/state are auto-flushed.
Stream takes the per-Ctx action mutex for the duration of fn, so the fn body has the same exclusivity guarantees as an action handler: Signal/State writes don't race with concurrent action POSTs or with other Stream callbacks on the same Ctx.
The returned *Ticker lets the caller pause, resume, or change the cadence at runtime. It is safe to ignore the return value if those controls are not needed.
func (*Ticker) Pause ¶ added in v0.4.0
func (t *Ticker) Pause()
Pause stops further callbacks from firing until Resume is called. In-flight callbacks complete normally.
func (*Ticker) Resume ¶ added in v0.4.0
func (t *Ticker) Resume()
Resume restarts callbacks after a Pause. No-op on a running ticker.
func (*Ticker) SetInterval ¶ added in v0.4.0
SetInterval changes the tick cadence to d. The new interval takes effect on the next tick boundary; the current in-flight callback (if any) is unaffected. Non-positive d is a no-op.
func (*Ticker) Stop ¶ added in v0.4.0
func (t *Ticker) Stop()
Stop terminates the ticker permanently. After Stop returns, no further callbacks fire and the underlying goroutine exits — Pause/Resume on a stopped ticker are no-ops. Idempotent; calling Stop on an already- stopped ticker is safe.
Stop is the explicit-shutdown counterpart to Ctx disposal: use it when the user navigates away from a sub-region but the page itself stays mounted (e.g. closing a modal that owned a polling ticker).
type Upcaster ¶ added in v0.6.0
type Upcaster = func(old json.RawMessage) (json.RawMessage, error)
Upcaster migrates a stored event payload from version N to version N+1. It works on raw JSON at the codec boundary and is the value passed to RegisterEvent. Exported so callers can name the function type they pass (e.g. when building a step from a helper) rather than rely on an unnamed-in-godoc parameter type.
Source Files
¶
- action.go
- app.go
- appval.go
- backoff.go
- backplane.go
- bitset.go
- broadcast.go
- composition.go
- computed.go
- config.go
- crypto.go
- csp.go
- ctx.go
- descriptor.go
- document.go
- encoding.go
- eventenvelope.go
- file.go
- form.go
- group.go
- handler.go
- info.go
- inmemory.go
- kvstore.go
- local.go
- log.go
- metrics.go
- middleware.go
- onevent.go
- op.go
- push.go
- reconnect.go
- recover.go
- render.go
- runtime.go
- server.go
- sess.go
- shape_bool.go
- shape_map.go
- shape_num.go
- shape_slice.go
- shape_str.go
- signal.go
- signatures.go
- sse.go
- state.go
- stateapp.go
- stateappevents.go
- stateappevents_canary.go
- stateappevents_coldstart.go
- stateappevents_compact.go
- stateappevents_fold.go
- stateappevents_gap.go
- stateappevents_migrate.go
- stateappevents_projector.go
- stateappevents_snapshot.go
- statesess.go
- stream.go
- tailer.go
- util.go
- walker.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package backplanetest provides a parameterized conformance suite that any via.Backplane implementation must pass.
|
Package backplanetest provides a parameterized conformance suite that any via.Backplane implementation must pass. |
|
Package h is a Go-native DSL for HTML composition.
|
Package h is a Go-native DSL for HTML composition. |
|
internal
|
|
|
examples/auth
command
Auth demonstrates the typed-session helpers and middleware-driven authentication.
|
Auth demonstrates the typed-session helpers and middleware-driven authentication. |
|
examples/chat
command
Chat is a live multi-user chatroom in one file.
|
Chat is a live multi-user chatroom in one file. |
|
examples/counter
command
Counter demo for the typed-API surface.
|
Counter demo for the typed-API surface. |
|
examples/countercomp
command
Countercomp shows nested compositions: two independent counter cards inside one page.
|
Countercomp shows nested compositions: two independent counter cards inside one page. |
|
examples/counterscope
command
Counterscope demos the difference between tab-local and app-scoped state.
|
Counterscope demos the difference between tab-local and app-scoped state. |
|
examples/feed
command
Feed demo for the append-only Signal[[]T] surface: a bounded ring buffer streams random values to the browser five times per second, keeping the most recent 50.
|
Feed demo for the append-only Signal[[]T] surface: a bounded ring buffer streams random values to the browser five times per second, keeping the most recent 50. |
|
examples/greeter
command
Greeter demonstrates a server-side Signal[string] driven by two actions.
|
Greeter demonstrates a server-side Signal[string] driven by two actions. |
|
examples/maps
command
Maps is a server-driven world map: the camera, the markers, and a moving marker are all controlled from Go and pushed to the browser over SSE — no client JavaScript.
|
Maps is a server-driven world map: the camera, the markers, and a moving marker are all controlled from Go and pushed to the browser over SSE — no client JavaScript. |
|
examples/pathparams
command
Pathparams demonstrates path:"name" tag-driven decoding into typed fields.
|
Pathparams demonstrates path:"name" tag-driven decoding into typed fields. |
|
examples/picocss
command
Picocss demonstrates the picocss plugin with the typed-API surface.
|
Picocss demonstrates the picocss plugin with the typed-API surface. |
|
examples/sysmon
command
Sysmon is a live system monitor: CPU, RAM, disk I/O, and network throughput, streamed to the browser over SSE.
|
Sysmon is a live system monitor: CPU, RAM, disk I/O, and network throughput, streamed to the browser over SSE. |
|
examples/todos
command
Todos exercises a slice of items, list rendering with h.Each, an input + signal, StateSess-backed persistence across tabs, and a filter signal — without ever leaving Go.
|
Todos exercises a slice of items, list rendering with h.Each, an input + signal, StateSess-backed persistence across tabs, and a filter signal — without ever leaving Go. |
|
examples/upload
command
Upload demonstrates a typed via.File field driving a real multipart/form-data POST.
|
Upload demonstrates a typed via.File field driving a real multipart/form-data POST. |
|
sessbridge
Package sessbridge lets the via/sess package reach the unexported session KV methods on via.Session without via exporting an untyped Load/Store/Delete surface.
|
Package sessbridge lets the via/sess package reach the unexported session KV methods on via.Session without via exporting an untyped Load/Store/Delete surface. |
|
spec
Package spec holds the action-trigger plumbing types shared by the via and via/on packages.
|
Package spec holds the action-trigger plumbing types shared by the via and via/on packages. |
|
Package memevents is the in-memory fault-injection layer for testing any via.Backplane against the nastiness a real backend exhibits.
|
Package memevents is the in-memory fault-injection layer for testing any via.Backplane against the nastiness a real backend exhibits. |
|
Package mw provides the recommended HTTP middleware stack for via apps: request-id stamping, access logs, panic recovery, strict CSP, HSTS, and plain-HTTP → HTTPS redirects.
|
Package mw provides the recommended HTTP middleware stack for via apps: request-id stamping, access logs, panic recovery, strict CSP, HSTS, and plain-HTTP → HTTPS redirects. |
|
Package on builds reactive event-handler attributes that POST to via actions.
|
Package on builds reactive event-handler attributes that POST to via actions. |
|
plugins
|
|
|
echarts
Package echarts provides an Apache ECharts plugin for the Via engine.
|
Package echarts provides an Apache ECharts plugin for the Via engine. |
|
maplibre
Package maplibre provides a MapLibre GL JS plugin for the Via engine — interactive vector maps driven from Go, with the camera, markers, and data layers updated over SSE.
|
Package maplibre provides a MapLibre GL JS plugin for the Via engine — interactive vector maps driven from Go, with the camera, markers, and data layers updated over SSE. |
|
picocss
Package picocss provides a PicoCSS plugin for the Via engine.
|
Package picocss provides a PicoCSS plugin for the Via engine. |
|
Package sess provides typed, per-browser session storage for via apps.
|
Package sess provides typed, per-browser session storage for via apps. |
|
Package vt (via test) holds testing helpers for via compositions.
|
Package vt (via test) holds testing helpers for via compositions. |
