Releases: go-via/via
Release list
v0.7.0
Highlights
The first slice of the road to 1.0 — stop the bleeding, stop the lying.
This release hardens the runtime where it was quietly broken and removes the
dishonest defaults: plugins no longer reach out to a CDN at boot, the
recommended CSP no longer bricks every click, SSE/backplane reconnects no
longer drop frames or die on the first blip, and a real-browser CI harness now
proves the client-side guarantees the unit harness structurally can't see. The
ratified five-phase v1.0 plan ships alongside as ROADMAP.md.
Breaking
Pre-1.0, so these ride the minor bump. See MIGRATION.md for before/after snippets.
NumOps.Min/Maxremoved →AtLeast/AtMost/Clamp(#133). The old
names were inverted relative to themath.Min/Maxintuition they invited
(Min(lo)raised the value). The replacements state their effect, and
Clamp(lo, hi)confines both ends in one verb.mw.CSP()now emits'unsafe-eval'(plusframe-ancestors 'self')
(#133). Via's bundled Datastar runtime compiles every expression with
Function(); the old recommended policy threwEvalErroron every handler.
If you hard-coded the previous policy string, adopt the new one (it still
only authorizes eval inside already-admitted same-origin/nonce'd script).- Plugin assets are embedded, not fetched (#133). The boot-time CDN fetch
(and its panic) is gone; plugins serve a pinnedgo:embedbuild from a
content-hashed same-origin path. To use a CDN now, opt in explicitly with
WithCDN(url, integrity)— SRI is mandatory. - Dev composition checks are on by default (#119, #124) — by-value
child-clobber is caught out of the box; disable in production with
WithoutDevChecks().
New
vtbrowser(#133) — a real headless-Chrome test harness in its own Go
module (core deps untouched), wired into a dedicated CI job. Proves Datastar
eval, SSE→DOM morph patching, focus preservation, and the reconnect-banner
lifecycle. Skips without a browser;VIA_BROWSER_REQUIRED=1makes CI fail
instead of silently skip.- Embedded plugin assets (#133) — Pico 2.1.1, ECharts 6.0.0, MapLibre
5.24.0 (+ licenses) served at content-hashed immutable paths; zero network
I/O at registration.WithCDN(url, integrity)/WithSource(path)opt-outs. - Health & readiness probes (#114) — default
GET /livez/healthz
/readyz, served before the session/middleware chain;/readyzflips to 503
onShutdownso a load balancer drains the pod cleanly.WithoutHealthEndpoints()to disable. - Client-side SSE reconnect manager (#122, #126) — a visible
"Reconnecting…" banner, page reload on exhausted retries, and a
data-via-connectionattribute (online/connecting/offline) on<html>
for your own indicator.WithoutSSEReconnect()to supply your own. WithMaxSessions(n)(#113) — bound the live session map against a
visitor flood, sibling toWithMaxContexts.WithStrictDecode()(#116) — reject lossy client-signal decodes instead
of silently coercing.WithVerboseErrors()(#115) — surface real panic messages to the client
in dev.OnEventbounded retry + poison-skip (#120) —WithMaxAttempts(n)opts a
failing record into being skipped afterntries (default: retry forever,
loudly).- apidiff CI gate (#127) + API-stability contract (#125) — incompatible
public-API changes fail the build against a committed baseline;stability.md
documents the stable-core vs. experimental tiers. via.tab.unknownmetric (#118) — detect non-sticky load-balancer routing.- v1.0
ROADMAP.md+MIGRATION.md(#133) — the ratified plan, and a
CI-coupled migration log (a public break with no anchored entry fails the build).
Fixed
- SSE drainQueue no longer loses frames (#133) — failed writes are
compare-cleared, not dropped; reconnects re-ship a coalesced server-pushed-signal
patch without clobbering client-local signals. - Backplane tailers unified on one reconnect loop (#133) — jittered
backoff, boot-retry, graceful stop. The changes/broadcast tailers previously
died permanently on the first blip. Newtailer_up/tailer_reconnectmetrics. - Reconnect banner clears passively after a successful reconnect (#129) and
no longer swallows clicks; correctaria-live(#132). Signal/LocalSignalAttr/Class/Styleused a dead hyphen binding
syntax Datastar silently ignored — fixed to the colon form (#128).- Wrong-pod actions recover instead of 404 (#123) — action/SSE recovery symmetry.
- Cancellation/teardown hardening sweep (#131) and in-flight backplane I/O
cancelled onShutdown(#111). - Jittered backoff between CAS retries (#117).
- Browser-harness consolidation (#134) —
internal/browsertestfolded into
vtbrowser; the #128 regression guard preserved.
Full Changelog: v0.6.1...v0.7.0
v0.6.1
Highlights
Broadcasts go cluster-wide. With a backplane wired via WithBackplane, the
App.Broadcast* family now reaches live tabs on every pod, not just the
one the call landed on — site-wide notices, maintenance banners, and
coordinated state invalidation finally work across a horizontally-scaled
deployment. Single-pod apps are unchanged.
Breaking
The notification verb is renamed — recompile and adjust call sites (no
deprecated alias):
Ctx.Toast→Ctx.NotifyandApp.BroadcastToast→
App.BroadcastNotify(#110). The public verb now names intent rather
than rendering, leaving room for future surfaces under one name; the
concrete toast renderer is unchanged.Ctx.Notifyis single-tab;
App.BroadcastNotifyis its cluster-wide counterpart.
New
- Cross-pod
Broadcast(#110) — a newvia.broadcastEventLog feed
carries the payload itself; each pod tails it from the feed HEAD and
applies to its live tabs. Append-only/uniform delivery means the
originating pod sees a notice exactly once, and a late-joining pod never
replays notices issued before it booted (broadcast is ephemeral and
best-effort — no Store cell, no reconcile, no monotone gate). Cross-pod
delivery activates only when a backplane is wired; the in-process default
stays pod-local.BroadcastNotify,BroadcastSignals, and
BroadcastSignal[T]all inherit cross-pod delivery, with toast XSS
escaping preserved across the feed. Return value remains this pod's
live-tab count (the cluster-wide total is unknowable synchronously).
Full Changelog: v0.6.0...v0.6.1
v0.6.0
Highlights
A hardening + API-cleanup release on top of the v0.5.0 backplane work. No
new subsystems — the surface gets tighter, safer, and better documented.
Pre-1.0, so the API trims below ride in this minor bump.
WithRequestTooLarge — install an http.Handler for the 413 a body
cap trips, the way WithNotFound handles 404. The limit fires in
MaxBytesReader before any action runs, so this is the only seam to turn
the bare "request too large" into a friendly response (e.g. bounce an
oversize upload back to its form with a flash message). Covers action
POSTs; an oversize SSE-close body still gets the bare 413 (#107).
Breaking
Public API surface trimmed — recompile and adjust call sites:
Ctx.Patchis now a method —ctx.Patch().Elements(...)/
ctx.Patch().Signal(...), not field access (#98).on.Optionre-exported fromoninstead of leaking
internal/spec— downstream code can name the option type (#99).- Internal wiring removed from the public API surface (#97).
New
WithMaxUploadSize— separate cap formultipart/form-databodies
(default 32 MiB), distinct fromWithMaxRequestBody(1 MiB JSON cap),
since file parts inflate the body past what a typed-signal payload
needs (#107).- Bundled Datastar client updated to v1.0.2 (#102).
Fixes
- showcase: avatar uploads rejected real photos — cap raised 4 MiB →
10 MiB, with a core regression test (#106). - hardening: adversarial-review pass across the full
viasurface
(#104) andviashowcase(validation, dedup, security, correctness)
(#103); hygiene sweep on the backplane projector + test alignment (#96).
Docs
- Per-function godoc on every
helement + attribute constructor (#101). - Option-type export idiom added to
CONVENTIONS.md(#100). - Cohesion passes: Datastar framing, dropped council/design records
(#108); upload-limit +Ctx.Patch+WithRequestTooLargedoc
alignment (#109). - Examples use the provided
hhelpers instead of rawh.Attr(#105).
Full changelog: v0.5.0...v0.6.0
v0.5.0
Highlights
State backplane — server-owned reactive state is now durable and
convergent across N processes without prescribing infrastructure. The two
long-standing non-goals (cluster runtime + restart survivability) are
closed. Purely additive: a nil backplane is byte-for-byte the existing
single-pod behaviour (#93).
StateAppEvents[E,V]— an event-log-backed sibling ofStateApp:
append immutable events, declare a pureFold, read the projection.
Per-(pod,key) projector is the sole fold path; cross-pod convergence by
tailing the log.Backplaneseam —StoreCAS-cell + ordered resumableEventLog- optional
Compactor.via.InMemory()is the base implnil
resolves to, so the interface is exercised on every single-pod run.
- optional
vianats— NATS JetStream reference backend (KV + stream), passes
the parameterized conformance suite (release-gating).- Clustered
StateApp/StateSess—Storeis the source of truth;
pods converge via a changes feed + periodic reconcile sweep; full-sid
fail-closed isolation. Sticky sessions no longer required for state
correctness. - Versioning — self-describing envelope + decode-only upcaster chain,
drop-on-undecodable, forward-incompat halt, epoch/offset-space-reset
detection. - Snapshots + compaction — disposable cache for uncompacted keys;
durable-genesis + seeded migration for compacted keys; consumer-aware- monotonic floor.
OnEventside-effect consumers — at-least-once, offset-committed,
idempotency-key.- GDPR crypto-shred —
KeyStore+ per-subject AES-256-GCM payloads +
EraseDataSubject(key drop + snapshot gen-invalidation; compacted+
erasure halts rather than truncates). WithFoldVerify— runtime fold-determinism gate that refuses to
compact a non-deterministic fold; per-fold divergence canary.
viashowcase "Signal" — a new flagship app exercising every Via
plugin (picocss, echarts, maplibre), the JetStream backplane
(StateAppEvents + OnEvent), Postgres-backed auth/profiles/avatars,
cross-page theme persistence, graceful shutdown, /healthz, and a 3-pod
docker-compose deployment behind a sticky HAProxy LB. Its own module
(replace -> ../, ../vianats) (#95).
chatcluster example — the single-node chat example runs as multiple
nodes behind a sticky-cookie HAProxy LB with JetStream NATS, demonstrating
cross-node StateAppEvents convergence. Docker-compose stack and
tag-gated compose e2e included (#95).
New
- docs:
docs/distributed-state.md(backplane architecture) and
docs/showcase.md(Signal), with cross-links from home,why-via, and
reactive-state(#95). - docs: load profile + full metrics catalogue,
why-viaclarity /
honesty pass, staleness sweep (#93).
Fixes
- backplane: re-seed a lagging projector on a compaction gap; snapshot
writes are monotonic inCoveredOffset. A fast pod compacting the
shared log previously truncated a lagging peer (#95). - viashowcase: close CodeQL "potentially unsafe quoting" on
Host.Noticeby reshaping the broadcast snippet to the established
IIFE/JSON.parse/textContentpattern; the title is no longer dropped
between two raw JS string fragments (#95).
Other
- ci: skip the Go pipeline on docs-only changes (the
Build and Test
job still always reports as a required check) (#94). - chore: satisfy golangci-lint (
paralleltest+staticcheck); gofmt- errcheck fixes (#93).
Full changelog:
v0.4.2...v0.5.0
v0.4.2
Highlights
MapLibre GL plugin (plugins/maplibre) — interactive vector maps driven entirely from Go over SSE, no client JavaScript (#91):
- Construction — declarative
NewMapoptions; typed coordinatesLngLat{Lng,Lat}/At(lng,lat)andBounds{West,South,East,North}defuse the lng/lat & bbox swap foot-guns;WithMarkerstatic markers;GenerateFeatureIDs. - Outbound — camera (
FlyTo/EaseTo/JumpTo/SetCenter/FitBounds), markers, GeoJSON sources/layers,Call/WithMapOptionescape hatches. - Inbound gestures —
OnClick,OnMoveEnd,OnMarkerClick,OnMarkerDragEnd,OnFeatureClick, and a genericOnMapEvent; bound methods (thevia.Actionconstraint, no publicany) read a typedMapEventcarrying the live camera. - Styling — typed expression builders (
Get/FeatureState/Zoom/Boolean/Case/Interpolate/Step) +WhenHovered/WhenState;WithFeatureHoverclient-side highlight. - Dialogs —
ShowPopup/ShowPopupHTML/ClosePopup; HTML bodies and markerPopupHTMLtake anh.H(composable, andh.Tescapes user data).
Fixes
- sse: coalesce an action's patches into a single SSE frame (#90).
Other
Full changelog: v0.4.1...v0.4.2
v0.4.1
What's new
SSE reconnect resilience (#82, #87)
A dropped SSE stream no longer freezes the tab:
- Unknown
via_tabon reconnect (context-TTL sweep, deploy/restart): the server re-bootstraps a fresh tab over the same stream — route recovered from the tab-id prefix, path/query params from theReferer,OnInit/OnConnectrun, fresh signal seed (newvia_tab) + full-view replace. No page flash; in-memory tab state starts fresh. - Degraded path (param route w/ no usable
Referer, or app atWithMaxContextscapacity): an explicitwindow.location.reload()is pushed instead of a silent freeze. - Forged tab ids still 404 — junk traffic can't mint contexts; the fresh id is always minted server-side, preserving the
via_tab-as-CSRF-token model. - Known-tab reconnects re-ship the current view (elements only — client-side signal state survives the blip), so clients that drifted while disconnected converge back to server truth.
- New metrics:
via.sse.resync,via.sse.recover{mode}.
Patch-queue fix: stale render no longer wins on reconnect (#87)
A disconnected tab (browsers close SSE on hidden tabs) accumulating several re-renders used to drain them oldest-wins — a refocused tab could visibly rewind to stale state. The auto-render is now kept latest-only; user-explicit Patch.Elements pushes stay authoritative, and a panicking view can no longer erase the last good queued frame.
⚠️ Breaking (#84)
refactor!: remove any from the public API where it erased type safety:
Session.Load/Store/Deleteremoved from the public API — the typedvia/sesspackage is the only access path (SessionkeepsRotate).h.Defaultnow requires an explicit type argument:h.Default[K](node).on.*helpers,sess.Get/sess.Clear, andh.Switch/h.Caseare now generic with union constraints — previously-compiling wrong-typed arguments are compile errors.- New typed
via.BroadcastSignal[T]; string-keyed signal writes remain as the documented escape hatch.
Also
- Compositions guide (nesting, props, lifecycle) (#83)
- Test isolation: stop sharing
http.DefaultTransportacross parallel tests (#85) - CI: run pull_request workflows only for PRs targeting main (#86)
- Docs/site polish (#74–#81)
Known issues (tracked)
- The bundled Datastar client does not retry a cleanly closed SSE stream and caps retries at 10 — a graceful-shutdown deploy can still leave a tab frozen until reload; a client-side re-arm is planned.
- An SSE handshake rejected with 403 (session mismatch, e.g. session TTL expiring under a live tab) also freezes; re-bootstrap-on-mismatch is under consideration.
Full changelog: v0.4.0...v0.4.1
v0.4.0
What's new
v0.4.0 is a ground-up rewrite around a typed reactive API. The framework
now expresses the client/server reactive split as a Go type the compiler
checks, ships as http.Handler, and drops in with no build step and no
hand-written JS.
Typed reactive API (headline)
Composition+Mount[C]/MountOn[C]— a composition is a plain
struct with aView(ctx *via.CtxR) h.Hmethod; one reflection walk per type
at mount, instances pooled viasync.Pool.- Four reactive shapes, owned by the field's type —
Signal[T]
(client + server) andStateTab[T]/StateSess[T]/StateApp[T]
(server only), each withNum/Bool/Str/Slice/Mapwrappers
and typed ops (Add,Toggle,Append, …). Which side owns a piece of
state is a declaration the compiler checks, not a convention you grep for. Appis anhttp.Handler— composes into any standard mux; SSE-only
transport.- Sub-packages —
h/(HTML DSL with fmt-style Datastar bindings),on/
(typed event handlers —on.Click(c.Inc)is a typed method reference, a typo
is a compile error),vt/(in-process test client). path:"x"struct tags decode into typed route params
(string/int/uint/float/bool). Misconfiguration panics at registration:
non-structC, missingView, unsupported field kind, or a path tag with no
matching route segment.- Coalescing patch queue — element patches are last-wins, signals merge by
key, scripts concatenate with isolated try/catch, redirects preempt pending
patches; bounded memory, no silent drops under burst load.
SSE liveness & reliability
- Connection-presence liveness — an open stream is never TTL-swept; an
always-on keepalive (floors to 25s) is the in-band detector of a vanished
half-open peer. Newvia.ctx.reapmetric, split out from
via.sse.disconnect(#67). - Brotli stream integrity — the raw SSE handshake comment no longer
corrupts aContent-Encoding: brstream for real browsers (#66). via.sse.disconnectis now labeled with a documentedreason(#58).
Security
- Session cookie
Secureby default; opt out only deliberately (#52). - Open-redirect guard — unsafe URLs are dropped in
Redirect, with the
guard hardened by tests (#46, #56). - CSP-nonced SSE pushes — script patches carry the document's captured
script-src 'nonce-…'so they survive a strict CSP (#59).
Correctness & robustness
- Recover
Viewpanics in both initial render and re-render (#50). - Panic on a nil
*CtxinSignal/Statemutation paths instead of a
confusing nil deref (#39). - Split
on.*panic messages by nil / top-level / closure cause (#47). - Guard
ctx.sessionwithatomic.Pointer(#51). float32formatting,sess.Clear(*CtxR), and a CI allocation gate (#59).- Styled Datastar toast replaces the blocking
alert()(#54).
Tooling, tests & CI
vt: isolatehttp.TransportperClientto stop parallel-test flakes
(#42).- Enforce
t.Parallelvia theparalleltestlinter; broad coverage expansion
(#53, #56). - CI no longer double-runs on push + PR; a guard rejects committed binaries
(#44, #61, #67).
Documentation & branding
- Docs site at go-via.github.io/via —
just-the-docs with a custom amber brand scheme (#57, #63, #65). - Brand kit under
branding/— theme-aware wordmark / bolt / app-icon SVGs
and an animated APNG, wired into an animated README banner and the docs logo +
favicon (#68–#73). - Modernized README down to a single counter quickstart that mirrors the
scope-demo gif (StateTabvsStateApp).
Breaking
- The public API is replaced by the typed reactive API above. Code written
against the v0.2/v0.3 surface must migrate toComposition+Mount[C]and
theSignal/Statehandles. See the
tutorial and
reactive-state guide.
Full diff: v0.3.0...v0.4.0
v0.3.0
What's new
Security
- Session ID entropy — raised from 32 to 256 bits
- Per-Ctx session binding —
handleAction,handleSSE, andhandleSSEClosereject requests whose session cookie does not match the session that created the Ctx; thevia_tabID continues to act as the anti-CSRF nonce
Reliability
- Coalescing patch queue — replaces the 64-slot chan patch; element patches last-wins, signals merge by key, scripts concatenate with per-script try/catch wrapping (order preserved, isolated failures), redirects preempt other pending patches; bounded memory, no silent drops under burst load
Session options
- WithSecureCookies() — marks the session cookie Secure
- WithContextTTL(d) — idle Ctx sweep (default 15min) to prevent per-tab context leaks when browsers close without firing the beforeunload beacon
SSE reliability
- WithSSEHeartbeat(d) — no-op SSE frame (default 25s) keeps the connection alive past reverse-proxy idle timeouts (nginx/ALB/Cloudflare defaults ~60s) and surfaces dead sockets
Breaking change
- ctx.W and ctx.R fields replaced by
ctx.Writer()andctx.Request()accessors that return nil (with a warn log) when called outside action scope
v0.2.4
What's new
Core
- App-level signals —
AppSignal[T](app, displayID, initial)creates reactive signals scoped to the app with per-tab value isolation; returns an exportedAppSignalHandle[T]for typed Get/SetValue - Page-load init —
cmp.Initnow runs on every page load (GET handler) before the view renders, instead of on SSE connect; enables signal setup that flows into the initial HTML - Signal value in initial HTML —
initialSigsloop reads fromctx.signalValuesso values set in Init appear in the first render, not as a follow-up SSE patch
Picocss plugin
- App-level signal registration — dark mode and theme signals registered via
AppSignalinstead of a separatedata-signalsmeta tag - Typed Get/Set API —
picocss.SetDarkMode(ctx, mode),picocss.GetDarkMode(ctx),picocss.SetTheme(ctx, theme),picocss.GetTheme(ctx) - Zero-flash theming — blocking inline script resolves dark mode and theme from
data-signalssynchronously before CSS loads
Auth example
- Layout init sets user preferences via
picocss.SetDarkMode/SetTheme - Profile actions use typed plugin API instead of
MarshalAndPatchSignals - View is read-only — no more signal mutation during render
v0.2.3
What's new
Core
- Sessions — cookie-based sessions with
SetSess,GetSess,ClearSess, configurable TTL viaWithSessionTTL - Session TTL sweep — background goroutine expires idle sessions at
TTL/2intervals - Middleware —
app.Use()and group-scopedgroup.Use()with short-circuit support - Route groups —
app.Group(prefix)with scoped middleware and layouts - Layouts —
app.Layout()andgroup.Layout()withcmp.Content(ctx)slot - User-scoped state —
State(cmp, val, WithScopeUser())shares state across tabs, isolated between sessions - Action mutex — serialized action execution per context prevents data races
- W/R escape hatch —
ctx.Wandctx.Ravailable in actions for raw HTTP access - Redirect —
ctx.Redirect(url)navigates the browser via SSE
Examples
- Auth example — full register/login lifecycle with sessions, protected routes, user preferences (dark mode system/light/dark + 5 picocss themes), flash messages, and middleware-based auth guard