Evaluate stateless architecture for Via's context model #19
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Via's current architecture stores application state in server memory (
*Contextper browser tab). This works but creates several architectural tensions that are worth re-evaluating as Via matures.Problems to Address
1. Ephemeral state in a durable world
*Contextholds the view function, signals, and action closures in memory. Server restart = total state loss. The only recovery iswindow.location.reload()(via.go L569-573). For simple pages this is acceptable, but for pages with significant UI state (filters, open drawers, form progress), it's disruptive.Alternative: State lives in Datastar signals (client-side) and the database (server-side). The
*Contextbecomes a thin SSE pipe. Server restart → SSE reconnects → client re-fetches via existingdata-init/data-on-signal-patchbindings. No state loss.2. Closures as the state graph
c.Action(func() { ... })captures variables from the enclosing scope. The state graph — which actions share which variables — is implicit in closure captures. It can't be inspected, serialized, or debugged at runtime. As pages grow, this becomes the hardest kind of code to reason about.Alternative: Actions become registered HTTP handlers that receive signal values as parameters. State comes from the request + database, not from captured closure variables. The state graph is explicit in function signatures.
3. Bidirectional signal synchronization
Signals exist in two places: client-side Datastar signals and server-side
sync.Map. They sync browser→server on every action (injectSignals) and server→browser on everySync()/SyncSignals(). This creates staleness windows and O(n) overhead per interaction.Alternative: Signals live exclusively on the client. The server reads them from request parameters when needed and pushes changes via
SyncSignals()when needed. One-way data flow in each direction, no server-side mirror.4. Horizontal scaling ceiling
In-memory
*Contextobjects require sticky sessions for multi-server deployments. Not an issue for single-server apps, but it's an architectural ceiling.What should NOT change
c.Sync()re-rendering the full view + morph diffing — this is the right abstraction. The simplicity of "state changed → Sync() → done" outweighs the bandwidth cost, especially with Brotli compression on the SSE connection.beforeunloadcleanup are all solid.c.Signal(),c.Action(),c.View(),c.Sync()is an intuitive API.Possible Direction
Keep the ergonomic API surface but change the underlying model:
c.Signal()→ creates a client-side-only Datastar signal (no server mirror)c.Action()→ registers an HTTP handler that receives signals as params (not a closure)c.View()→ defines the initial server render;c.Sync()re-renders and pushes the full view through the SSE pipe (unchanged)SyncSignals(), letting client reactivity handle re-fetchingThis preserves Via's strengths (simple API, full-view morph, SSE push) while eliminating the state management risks (ephemeral closures, bidirectional sync, scaling ceiling).
Context
This came out of planning persistent SSE for MUREP. Building MUREP's
internal/ssepackage (stateless push pipe) highlighted the contrast with Via's stateful context model. Both approaches have merit — this issue is about evaluating whether Via should evolve toward the stateless model for its core while keeping the ergonomic API.