Skip to content

Releases: go-via/via

v0.7.0

Choose a tag to compare

@joaomdsg joaomdsg released this 12 Jun 13:38
8bd5f1d

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/Max removed → AtLeast/AtMost/Clamp (#133). The old
    names were inverted relative to the math.Min/Max intuition 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' (plus frame-ancestors 'self')
    (#133). Via's bundled Datastar runtime compiles every expression with
    Function(); the old recommended policy threw EvalError on 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 pinned go:embed build 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=1 makes 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; /readyz flips to 503
    on Shutdown so 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-connection attribute (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 to WithMaxContexts.
  • WithStrictDecode() (#116) — reject lossy client-signal decodes instead
    of silently coercing.
  • WithVerboseErrors() (#115) — surface real panic messages to the client
    in dev.
  • OnEvent bounded retry + poison-skip (#120) — WithMaxAttempts(n) opts a
    failing record into being skipped after n tries (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.unknown metric (#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. New tailer_up/tailer_reconnect metrics.
  • Reconnect banner clears passively after a successful reconnect (#129) and
    no longer swallows clicks; correct aria-live (#132).
  • Signal/LocalSignal Attr/Class/Style used 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 on Shutdown
    (#111).
  • Jittered backoff between CAS retries (#117).
  • Browser-harness consolidation (#134) — internal/browsertest folded into
    vtbrowser; the #128 regression guard preserved.

Full Changelog: v0.6.1...v0.7.0

v0.6.1

Choose a tag to compare

@joaomdsg joaomdsg released this 10 Jun 17:33
260bcbb

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.ToastCtx.Notify and App.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.Notify is single-tab;
    App.BroadcastNotify is its cluster-wide counterpart.

New

  • Cross-pod Broadcast (#110) — a new via.broadcast EventLog 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

Choose a tag to compare

@joaomdsg joaomdsg released this 10 Jun 15:34
cb7f5cb

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.Patch is now a methodctx.Patch().Elements(...) /
    ctx.Patch().Signal(...), not field access (#98).
  • on.Option re-exported from on instead 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 for multipart/form-data bodies
    (default 32 MiB), distinct from WithMaxRequestBody (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 via surface
    (#104) and viashowcase (validation, dedup, security, correctness)
    (#103); hygiene sweep on the backplane projector + test alignment (#96).

Docs

  • Per-function godoc on every h element + 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 + WithRequestTooLarge doc
    alignment (#109).
  • Examples use the provided h helpers instead of raw h.Attr (#105).

Full changelog: v0.5.0...v0.6.0

v0.5.0

Choose a tag to compare

@joaomdsg joaomdsg released this 06 Jun 22:58
e6ec133

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 of StateApp:
    append immutable events, declare a pure Fold, read the projection.
    Per-(pod,key) projector is the sole fold path; cross-pod convergence by
    tailing the log.
  • Backplane seamStore CAS-cell + ordered resumable EventLog
    • optional Compactor. via.InMemory() is the base impl nil
      resolves to, so the interface is exercised on every single-pod run.
  • vianats — NATS JetStream reference backend (KV + stream), passes
    the parameterized conformance suite (release-gating).
  • Clustered StateApp / StateSessStore is 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.
  • OnEvent side-effect consumers — at-least-once, offset-committed,
    idempotency-key.
  • GDPR crypto-shredKeyStore + 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-via clarity /
    honesty pass, staleness sweep (#93).

Fixes

  • backplane: re-seed a lagging projector on a compaction gap; snapshot
    writes are monotonic in CoveredOffset. A fast pod compacting the
    shared log previously truncated a lagging peer (#95).
  • viashowcase: close CodeQL "potentially unsafe quoting" on
    Host.Notice by reshaping the broadcast snippet to the established
    IIFE/JSON.parse/textContent pattern; 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

Choose a tag to compare

@joaomdsg joaomdsg released this 03 Jun 20:09
7fe6ba8

Highlights

MapLibre GL plugin (plugins/maplibre) — interactive vector maps driven entirely from Go over SSE, no client JavaScript (#91):

  • Construction — declarative NewMap options; typed coordinates LngLat{Lng,Lat}/At(lng,lat) and Bounds{West,South,East,North} defuse the lng/lat & bbox swap foot-guns; WithMarker static markers; GenerateFeatureIDs.
  • Outbound — camera (FlyTo/EaseTo/JumpTo/SetCenter/FitBounds), markers, GeoJSON sources/layers, Call/WithMapOption escape hatches.
  • Inbound gesturesOnClick, OnMoveEnd, OnMarkerClick, OnMarkerDragEnd, OnFeatureClick, and a generic OnMapEvent; bound methods (the via.Action constraint, no public any) read a typed MapEvent carrying the live camera.
  • Styling — typed expression builders (Get/FeatureState/Zoom/Boolean/Case/Interpolate/Step) + WhenHovered/WhenState; WithFeatureHover client-side highlight.
  • DialogsShowPopup/ShowPopupHTML/ClosePopup; HTML bodies and marker PopupHTML take an h.H (composable, and h.T escapes user data).

Fixes

  • sse: coalesce an action's patches into a single SSE frame (#90).

Other

  • examples: redesign counterscope UI with Via branding (#88).
  • ci: bump Go toolchain to 1.26.4 (#89).

Full changelog: v0.4.1...v0.4.2

v0.4.1

Choose a tag to compare

@joaomdsg joaomdsg released this 02 Jun 17:55
de5a9b9

What's new

SSE reconnect resilience (#82, #87)

A dropped SSE stream no longer freezes the tab:

  • Unknown via_tab on 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 the Referer, OnInit/OnConnect run, fresh signal seed (new via_tab) + full-view replace. No page flash; in-memory tab state starts fresh.
  • Degraded path (param route w/ no usable Referer, or app at WithMaxContexts capacity): an explicit window.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/Delete removed from the public API — the typed via/sess package is the only access path (Session keeps Rotate).
  • h.Default now requires an explicit type argument: h.Default[K](node).
  • on.* helpers, sess.Get/sess.Clear, and h.Switch/h.Case are 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.DefaultTransport across 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

Choose a tag to compare

@joaomdsg joaomdsg released this 31 May 14:57
ca92c46

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 a View(ctx *via.CtxR) h.H method; one reflection walk per type
    at mount, instances pooled via sync.Pool.
  • Four reactive shapes, owned by the field's typeSignal[T]
    (client + server) and StateTab[T] / StateSess[T] / StateApp[T]
    (server only), each with Num / Bool / Str / Slice / Map wrappers
    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.
  • App is an http.Handler — composes into any standard mux; SSE-only
    transport.
  • Sub-packagesh/ (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-struct C, missing View, 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. New via.ctx.reap metric, split out from
    via.sse.disconnect (#67).
  • Brotli stream integrity — the raw SSE handshake comment no longer
    corrupts a Content-Encoding: br stream for real browsers (#66).
  • via.sse.disconnect is now labeled with a documented reason (#58).

Security

  • Session cookie Secure by 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 View panics in both initial render and re-render (#50).
  • Panic on a nil *Ctx in Signal/State mutation paths instead of a
    confusing nil deref (#39).
  • Split on.* panic messages by nil / top-level / closure cause (#47).
  • Guard ctx.session with atomic.Pointer (#51).
  • float32 formatting, sess.Clear(*CtxR), and a CI allocation gate (#59).
  • Styled Datastar toast replaces the blocking alert() (#54).

Tooling, tests & CI

  • vt: isolate http.Transport per Client to stop parallel-test flakes
    (#42).
  • Enforce t.Parallel via the paralleltest linter; 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 (StateTab vs StateApp).

Breaking

  • The public API is replaced by the typed reactive API above. Code written
    against the v0.2/v0.3 surface must migrate to Composition + Mount[C] and
    the Signal/State handles. See the
    tutorial and
    reactive-state guide.

Full diff: v0.3.0...v0.4.0

v0.3.0

Choose a tag to compare

@joaomdsg joaomdsg released this 18 Apr 22:39

What's new

Security

  • Session ID entropy — raised from 32 to 256 bits
  • Per-Ctx session bindinghandleAction, handleSSE, and handleSSEClose reject requests whose session cookie does not match the session that created the Ctx; the via_tab ID 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() and ctx.Request() accessors that return nil (with a warn log) when called outside action scope

v0.2.4

Choose a tag to compare

@joaomdsg joaomdsg released this 09 Apr 14:01

What's new

Core

  • App-level signalsAppSignal[T](app, displayID, initial) creates reactive signals scoped to the app with per-tab value isolation; returns an exported AppSignalHandle[T] for typed Get/SetValue
  • Page-load initcmp.Init now 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 HTMLinitialSigs loop reads from ctx.signalValues so 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 AppSignal instead of a separate data-signals meta tag
  • Typed Get/Set APIpicocss.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-signals synchronously 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

Choose a tag to compare

@joaomdsg joaomdsg released this 08 Apr 18:32

What's new

Core

  • Sessions — cookie-based sessions with SetSess, GetSess, ClearSess, configurable TTL via WithSessionTTL
  • Session TTL sweep — background goroutine expires idle sessions at TTL/2 intervals
  • Middlewareapp.Use() and group-scoped group.Use() with short-circuit support
  • Route groupsapp.Group(prefix) with scoped middleware and layouts
  • Layoutsapp.Layout() and group.Layout() with cmp.Content(ctx) slot
  • User-scoped stateState(cmp, val, WithScopeUser()) shares state across tabs, isolated between sessions
  • Action mutex — serialized action execution per context prevents data races
  • W/R escape hatchctx.W and ctx.R available in actions for raw HTTP access
  • Redirectctx.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