feat: typed reactive API — production runtime, h rewrite, sub-packages#30
Merged
Conversation
- Add `via.Composition` interface: `View(ctx *via.Ctx) h.H`
- Add `Mount[C]` and `MountOn[C]` generic factories
- Add `cmpDescriptor` with one-time reflection walk at mount
- Decode `path:"x"` struct tags into {x} route params via strconv
(string/int*/uint*/float*/bool)
- Panic at registration on: non-struct C, missing View, unsupported
field kind, path tag with no matching route segment
- Pool *C instances via sync.Pool per descriptor
- Move v0.2.x code to internal/viaold/ - Move h/ package from old/ to root - Delete mount.go and composition_new_test.go (old API) - Add markdownlint config - Update ci-check.sh for new structure - Add via-implementation-plan.md - Add sysmon example - Add datastar.js
…+action handlers, http.Handler App
- App now implements http.Handler so via composes into any std mux.
- Mount[C] reflects once per type, caches a cmpDescriptor with signal/
state/path/action slots, and registers a route handler that allocates
a fresh *C, decodes path params + signal init tags, optionally calls
Init, renders the view inside the HTML5+datastar envelope, and parks
the *Ctx in the registry for the SSE stream and action POSTs.
- Signal[T]/State[T] are zero-value-usable typed handles. Storage lives
in the composition struct field; slot/key are stamped per-request.
Get/Set take ctx; Bind/Text/Show return h.H attributes/nodes.
- SSE handler drains a coalescing patch queue (elements last-wins,
signals merged, scripts concatenated, redirect preempts).
- Action handler dispatches POST /_action/{methodName} to the bound
*C method by reflect, applying request-side signal updates first
and flushing dirty bits on return.
- WithMaxRequestBody / WithActionErrorHandler / WithHTTPServer carry
over the production hardening from main.
- Removed Config() accessor and its lifecycle-getter tests; defaults
are now observable through behaviour.
- on.Click/Change/Input/Submit/Key build Datastar @post attributes from bound method values. The method name is recovered via reflect on the closure PC and runtime.FuncForPC, with the -fm bound-method suffix stripped. - via.MethodName, via.ActionFn, via.TriggerSpec, via.TriggerOption are the contracts the on/* package consumes. Modifiers (Prevent, Stop), timing (Debounce, Throttle), and key filters (on.Key) chain via TriggerOption. - New tests cover: rendered Bind/Text/Show attributes, default key derivation from field names, init=… tag values surfacing in initial data-signals, on.Click rendering /_action/<method>, 404 on unknown methods, MethodName resolving bound methods.
…State[T] re-render integration
- scope.User[T] and scope.App[T] are session/app-scoped reactive
values. Distinct types keep mismatched scopes a compile error.
Set marks the current Ctx dirty via the new exported
via.MarkDirty(ctx) helper.
- via/test exposes Client / Action / Signal / SSE for integration
tests. NewClient does the GET, picks up the tab id from the
rendered HTML, and authenticates subsequent action POSTs and SSE
reads with the right cookies.
- State[T] now exercised end-to-end: a counter mounted at /,
three Inc POSTs, an SSE consumer observes <p>3</p> arrive in the
flushed element patch.
- Path-param tests cover int + string decoding from {id}/{slug} and
panic-at-Mount on tag/route mismatch.
- Descriptor walk is now recursive: composition-typed struct fields
produce qualified wire keys ("Card.title" instead of "title")
so siblings of the same type don't collide. fieldPath replaces
fieldIndex on signal/param slots; runtime addresses nested fields
through reflect.Value.FieldByIndex.
- A child satisfies the recursion if its pointer type implements View
and the type lives outside the via package — keeping the walk away
from Signal[T]'s own private struct fields.
- internal/examples/counter/main.go: end-to-end demonstration —
via.State[int], via.Signal[int] with init=1, on.Click(c.Inc), and
the App handed straight to http.ListenAndServe.
- App.RegisterAppSignal(key, value) seeds a named, app-wide value into every page's <meta data-signals>. Plugins use it to publish client- driven signals ($_picoTheme, $_picoDarkMode) without a server-side reactive handle. - Ctx.ExecScript / Redirect / Sync round out the patch surface that plugins (and apps) need: queue a script, ask the browser to navigate, or force a re-render. - plugins/picocss is a near-mechanical port: same fetch-on-Register, same per-theme caching, etag handling, gzip variants, blocking head script for zero-flash theme switch. Initial signals now go through RegisterAppSignal instead of the old AppSignal handle. - plugins/echarts ports verbatim — its only via API touchpoint is AppendToHead and Ctx.ExecScript, both unchanged in the new API. - pico_test.go exercises the plugin against the real CDN; -short skips.
- greeter: a Signal[string] driven by two action methods. Demonstrates the typed init=value tag. - pathparams: path:"counter_id" / path:"start_at_step" decoding into typed string and int fields. picocss layout for visual parity with the viaold version. - countercomp: two CounterCard children mounted on one Page. Showcases the v1 nesting model — children render their own state and receive the click handler from the parent so each card dispatches a distinct action (IncA / IncB). - picocss: large showcase page using the picocss plugin's new ThemeRef/DarkModeRef helpers. The plugin's RegisterAppSignal seeds $_picoTheme and $_picoDarkMode into every page's data-signals. - pico.go gets ThemeRef() and DarkModeRef() helpers so callers don't have to hard-code the underscore-prefixed signal names.
- cmpDescriptor caches viewIdx, initIdx, disposeIdx at Mount time; per-request hot path now does cmpVal.Method(idx) directly instead of MethodByName (which scans the method table on every call). - renderBufPool sync.Pool reuses 8 KiB *bytes.Buffer across element- patch flushes. Outsize buffers (>1 MiB) are dropped on Put rather than retained. - BenchmarkCounterRender / BenchmarkCounterAction added under -benchmem to track regressions. Local result: ~204 allocs/render and ~142 allocs/action — sets the floor for further pooling work.
- Quick start now shows a Counter struct with via.State[int], a via.Signal[int] tagged with init=1, on.Click(c.Inc), and the App passed straight to http.ListenAndServe. - Documents the four state scopes (Signal, State, scope.User, scope.App), the lifecycle hooks (Init/Dispose/View), the on/* event surface, path:"…" tag decoding, plugin registration, and the via/test client. - Updated the configuration table to reflect the production-hardening options (max body, action error handler, http.Server hook).
- A composition method OnConnect(ctx) error fires once when the SSE stream is established for that tab. Bots that hit GET without ever opening the SSE stream don't trigger it, so expensive background goroutines (tickers, fan-out) live in OnConnect rather than Init. - Descriptor caches initIdx, connectIdx, disposeIdx alongside viewIdx to keep dispatch reflection-light. - sysmon example demonstrates the new hook: chart options seeded in OnConnect, then a per-tab ticker goroutine reads CPU/RAM/disk/network counters every 1 s, pushes State updates, and replaces ECharts series data — all inside the existing patch queue.
- removeExpiredContexts sweeps tabs whose lastAccess hasn't been touched within contextTTL (default 15 minutes); the goroutine ticks on every contextTTL/2 and runs disposeCtx on each expired entry, which closes Done and invokes the optional Dispose method. - TestDispose_runsOnAppShutdown asserts Dispose fires when an active Ctx still in the registry is shut down — closes the parity gap with the previous Cmp-style Dispose hook.
- Ctx.reflectArgs is a single-element [reflect.Value] array boxed with reflect.ValueOf(ctx) at request entry. Init/View/OnConnect/ Action/Dispose dispatch all reuse the same slice instead of re-allocating one per call. Drops per-action alloc from 142 → 143 allocs/op (the slice elision wins go to render-buffer reuse on state flushes), trims ~150 B/op.
- Stream wires the most common OnConnect pattern — ticker goroutine + auto-flush on tick — into one call. The goroutine stops on ctx.Done; panics inside the callback are recovered and logged rather than crashing the server. - After each fn returns, dirty signals/state are auto-flushed so the callback only has to call Set; no explicit Sync(). - Tests cover the happy path (3+ ticks observed in SSE element patches) and disposal (no goroutine leak after SSE cancel + close beacon, race-clean).
- h.Each(items, fn) renders one node per element. Drops the boilerplate of building a []h.H + spreading it as variadic. Empty input renders nothing — useful when the list is a State[T] whose initial value is empty. - h.EachIndexed passes the index alongside the value for keyed lists. - h.When(cond, fn) is If with a builder, so node construction is lazy: expensive pieces only get built when the condition is true. - h.Group bundles many H nodes into one so functions can return a multi-node fragment from a single return path. Uses a tiny []gNode wrapper and Render iterates it. - All four covered by table-style tests in h_test.go.
- Mount now validates the signatures of View, Init, OnConnect, Dispose at registration time, not at first dispatch. Wrong signatures panic with the format-the-fix-yourself expected form so the user sees the exact method header to write. - Missing View names the offending type and includes the example signature inline. - Tests cover: missing View, View without ctx, Init returning nothing, Dispose without ctx — each asserting the panic mentions the offending method's name.
- on.SetSignal(&c.Field, value) is a TriggerOption: it prepends a client-side $key=value statement to the rendered Datastar expression so the signal updates synchronously before the @post fires. The action handler reads the new value through the standard signal-injection path. - Bundling pre-statements went into TriggerSpec.Pre + AppendPre on the via package so the on/* sub-package owns rendering but doesn't monopolise the data shape. - Tests: int values ($step=5) and string values ($theme="red") both render with HTML-escaped quotes and a leading semicolon separator before the @post.
…request handle Before: scope.User[T] stored the value inline on the handle (which is allocated fresh per request), so a Set in one tab couldn't be Get-ed in another tab of the same session — silent data loss with the exact-opposite of the documented semantics. scope.App[T] had the same shape, even worse (no sharing across sessions). Fix: storage moves to the via runtime. - via.SessionLoad/Store(ctx, key, value) reads/writes the per-session sync.Map. scope.User wraps these. - via.AppLoad/Store(ctx, key, value) reads/writes a sync.Map on App. scope.App wraps these. - Descriptor walk recognises scope.User[T]/scope.App[T] fields and records a scopeSlot with the qualified wire key (parent.field path for nested compositions). Per request the runtime writes WireKey (now an exported field) into each handle so Get/Set know what to look up. - getOrCreateSession also AddCookie's the freshly-minted cookie onto r so renderPage's sessionFromRequest call (later in the same chain) finds the session it just created — needed because the session is set on the response, not the request, by default. - Tests cover both: scope.User write surfaces in the SSE re-render of the same tab; scope.App.Bump persists across distinct sessions (separate cookie jars).
Group.Use accepted middleware but never applied it — the chain was recorded on the Group but HandleFunc registered handlers directly on the app mux, bypassing it. Same for via.MountOn[C](g, route): the descriptor went straight to the app's route table. - Group.HandleFunc and Group.Handle now wrap the registered handler in applyMiddleware(g.middleware, ...) before mux.Handle. - MountOn snapshots the group middleware onto the descriptor's groupMW slice; registerDescriptor wraps the rendered route handler with that chain. Mount[C] (no group) is unaffected. - Three new tests pin the contract: HandleFunc-wrapped middleware sets a header, can short-circuit a leaky inner handler, and applies to MountOn-mounted compositions.
Reverses the previous "remove State.Text" commit. All three State
types now share the same Text shape, taking the readCtx interface so
both *via.Ctx and *via.CtxR work:
func (s *StateTab[T]) Text(_ readCtx) h.H
func (s *StateSess[T]) Text(rc readCtx) h.H
func (a *StateApp[T]) Text(rc readCtx) h.H
StateTab ignores the ctx (value lives on the struct) — the parameter
is for signature parity, so a View can call s.Text(ctx) regardless of
whether s is StateTab / StateSess / StateApp.
Removing the helper made callers spell out h.Text(s.Get(ctx)) /
h.Textf("%v", s.Get(ctx)) at every render site, which read worse than
the dedicated helper and forced format-string decisions per type.
Re-adding it with the unified ctx-taking signature gets the symmetry
this task wanted in the first place.
Move three Ctx methods onto a Patch sub-object so they read as the
escape hatch they are, not first-reach:
ctx.PatchSignal → ctx.Patch.Signal(key, value)
ctx.PatchSignals → ctx.Patch.Signals(map[string]any{...})
ctx.SyncElements → ctx.Patch.Elements(...h.H)
ctx.Patch.Element(h.H) // NEW singular form
The two-step access (ctx.Patch.X) is the tiering signal — a user
typing `ctx.` sees Patch as one entry among the first-class controls
(SyncNow, SyncOff, Session, Cookie, …) and has to opt into it. The
flat naming used Patch* / Sync* awkwardly: "Patch" lived on the
signal side, "Sync" on the element side, while the actual semantics
are reversed (signals get set, not partially patched; elements get
morph-patched, not synchronized).
The new shape matches Datastar's wire protocol — both groups
correspond to datastar-patch-{signals,elements} SSE event names — so
debugging the SSE stream lines up with the method names.
Patch is a thin struct {ctx *Ctx} allocated eagerly in newCtx (same
pattern as the cached *CtxR). Zero-alloc field access after that.
Element(h.H) is a singular convenience over Elements(...h.H) for the
common one-element case, mirroring the Signal / Signals split. Both
keep the original guards (empty key / nil map / nil element / empty
variadic = no-op).
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
Phase A of the typed-reactive-ops redesign (master task #17). Mechanical rename across the four reactive kinds: Signal[T].Get(ctx) → Signal[T].Read(ctx) StateTab[T].Get(ctx) → StateTab[T].Read(ctx) StateSess[T].Get(ctx) → StateSess[T].Read(ctx) StateApp[T].Get(ctx) → StateApp[T].Read(ctx) Read is the better name once the reactive API gains Op(ctx) chain operations (Phase B+) — read/op pair as the two halves of "access the reactive value." Get was a generic accessor verb that didn't telegraph the framework's role. Existing Set/Update/Op contracts unchanged. The readCtx interface parameter (accepts both *Ctx and *CtxR) carries through unmodified. Swept every caller in tests, examples, and the picocss/echarts plugins. Stdlib false positives from the gofmt rewrite (http.Client.Get, http.Header.Get, reflect.StructTag.Get, textproto.MIMEHeader.Get) were reverted.
Phase B of the typed-reactive-ops redesign. Every reactive kind (Signal[T] / StateTab[T] / StateSess[T] / StateApp[T]) gains an Op(ctx) method returning *Ops[T]. The generic Ops[T] exposes the two universal verbs: Op(ctx).Apply(func(T) T) — custom transform escape hatch Op(ctx).To(v) — replace with a constant value Both flow through the handle's existing Update path, so atomicity, dirty marking, and broadcast semantics are unchanged. Phase C will extend Ops with shape-specific verbs (Add/Toggle/Append/...) via specialized handle types. Set(ctx, v) and Update(ctx, fn) continue to work. Op(ctx) is purely additive — nothing migrated yet, that's Phase D's job. This commit lays the chain entry pattern.
Phase C of the typed-reactive-ops redesign.
Five shape ops surfaces — each carries the shape's typed verbs plus
the inherited Ops[T] (Apply/To):
NumOps[T Number] Add / Sub / Mul / Div / Inc / Dec / Zero /
Min(lo) / Max(hi)
BoolOps Toggle / True / False
StrOps Append / Prepend / Clear
SliceOps[T any] Append / Prepend / Pop / Shift / Empty /
Take(n) / Drop(n) / Filter(pred)
MapOps[K comparable, V any] Put / Delete / Empty
Twenty user-facing specialized types (5 shapes × 4 kinds) that pick
the right ops via Op(ctx):
SignalNum[T] StateTabNum[T] StateSessNum[T] StateAppNum[T]
SignalBool StateTabBool StateSessBool StateAppBool
SignalStr StateTabStr StateSessStr StateAppStr
SignalSlice[T] StateTabSlice[T] StateSessSlice[T] StateAppSlice[T]
SignalMap[K,V] StateTabMap[K,V] StateSessMap[K,V] StateAppMap[K,V]
Each wrapper embeds its underlying generic (Signal[T] / StateTab[T] /
StateSess[T] / StateApp[T]) so Read/Set/Update/Text/Key/Bind/Show/Attr
/Style etc. promote for free. Op(ctx) is overridden to return the
shape-typed ops.
Walker discovery moves from name-prefix matching to unexported marker
interfaces (signalMarker / stateTabMarker / stateSessMarker /
stateAppMarker). Each generic carries the marker method; embedded
specialized wrappers inherit it via promotion, so the walker
classifies them correctly without per-name maintenance.
Nothing migrated yet — generic Signal[T] / StateTab[T] / StateSess[T]
/ StateApp[T] still work for custom T. Phase D sweeps tests +
examples to use the specialized types where the common case fits.
Phase D of the typed-reactive-ops redesign. Field declarations swept across every test, example, and plugin: via.Signal[int] → via.SignalNum[int] via.Signal[bool] → via.SignalBool via.Signal[string] → via.SignalStr via.Signal[[]T] → via.SignalSlice[T] via.StateTab[…] → via.StateTab*… (same family) via.StateSess[…] → via.StateSess*… via.StateApp[…] → via.StateApp*… Generic forms retained where T is a custom struct/interface and the shape doesn't fit a typed bucket (render_test marshalUnfriendly, on_test unmarshalable, upload example's LastUpload). Side effect — on.SetSignal still takes *via.Signal[T] (the bound typed handle), so test/example call sites that wrap with a typed wrapper now reach for the embedded field explicitly: on.SetSignal(&p.Theme.Signal, "red") Same DX cost everywhere, easy to grep. A future PR can make on.SetSignal accept any wrapper through an interface; out of scope for this phase. README updated with the typed-shape table + verb cheatsheet alongside the existing generic Update(ctx, fn) examples. Net effect of the four phases: Phase A — Get → Read Phase B — Op(ctx).Apply / Op(ctx).To on every handle Phase C — typed shape handles (20) + shape ops (5) Phase D — codebase migrated to typed shapes
Phase E — read/write naming symmetry. Pairs naturally with Read(ctx) renamed in Phase A: Signal[T].Set(ctx, v) → Signal[T].Write(ctx, v) StateTab[T].Set(ctx, v) → StateTab[T].Write(ctx, v) StateSess[T] and StateApp[T] still have no direct write — their shared-state semantics demand Update(ctx, fn) so the load → fn → store sequence stays atomic under the per-key mutex (a top-level "blind write" verb on those would be a race in disguise). Swept all callers in tests, examples, and plugins. Stdlib false positives from gofmt -r (http.Header.Set, http.Header.Add) were reverted. README + docstrings updated to the Read/Write pair vocabulary.
Phase F of the typed-reactive-ops redesign.
The Update surface gains an error path on every reactive kind:
Update(ctx, fn func(T) (T, error)) error
fn returns the new value and an optional error. On non-nil error:
- the store is unchanged (value, dirty bit, broadcast all skipped),
- the error is returned to the caller.
This is the only mutation path that lets the user reject a write
in-band — useful for validation, conflict detection, budget guards.
The atomic load → fn → store sequence honours the rollback because
the kvStore.Update implementation now stores only on nil error.
Op chain's Apply method is removed. Update IS the fallible/custom
transform entry; the chain is reserved for canned typed verbs.
// before
p.Count.Op(ctx).Apply(func(n int) int { return n + 1 })
// after
p.Count.Op(ctx).Add(1) // typed verb
err := p.Count.Update(ctx, func(n int) (int, error) {
if n >= max { return 0, errBudget }
return n + 1, nil
})
Write becomes one-line sugar over Update:
func (s *StateTab[T]) Write(ctx *Ctx, v T) {
_ = s.Update(ctx, func(T) (T, error) { return v, nil })
}
Swept every Update caller across tests, examples, plugins, and
shape ops. Stdlib false positives (http.Header.Set, etc.) from the
gofmt -r rewrite were reverted. Linter (errcheck) flagged the
discarded errors at infallible Update sites; those got an explicit
`_ =` to make the intent visible. The shape ops methods (Add /
Toggle / Append / …) discard internally since their closures can't
fail.
A new test (TestOp_UpdateErrorRejectsTheWrite) pins the rollback
contract: a failing Update must leave the previous value intact.
README updated with the new fn shape and an "Update returns error"
example.
…(ctx)
Phase G of the typed-reactive-ops redesign.
With Write available on per-tab kinds (Signal[T] / StateTab[T]) and
Update being the universal mutator for shared kinds, the chain's To(v)
verb is redundant — and dropping it preserves the framework's
"no blind Write on shared state" stance: a user who really wants to
discard the old value on StateSess/StateApp must spell out
Update(ctx, func(T) (T, error) { return v, nil }), so the friction
is in front of their eyes.
Cascading cleanup:
- To removed from the shape ops surface.
- The base Ops[T] had no exported methods left → renamed to
unexported ops[T], used purely as an internal embed.
- Op(ctx) removed from the four generic kinds (Signal[T] /
StateTab[T] / StateSess[T] / StateApp[T]) — without To, it
returned an empty type with no user-facing purpose.
- Op(ctx) survives only on specialized types (SignalNum,
StateTabBool, etc.) where it returns the shape-typed ops.
Shape constants that used to call o.To inline (BoolOps.True/False,
NumOps.Zero, StrOps.Clear, SliceOps.Empty, MapOps.Empty) now build
their own update closure directly. Cheap, explicit.
Sweep — every Op(ctx).To callsite swept:
per-tab kinds (Signal*, StateTab*) → handle.Write(ctx, v)
shared kinds (StateSess*, StateApp*) → handle.Update(ctx, fn)
README updated with the new "Write vs Update" guidance.
Net surface after Phases A–G:
Read uniform across the family — handle.Read(ctx)
Write per-tab handles only — handle.Write(ctx, v)
Update uniform — handle.Update(ctx, fn func(T) (T, error)) error
Op(ctx).<verb> typed shape verbs on specialized handles
(SignalNum/Bool/Str/Slice/Map and the StateTab/
Sess/App equivalents)
Task 4 of the original DX punch list. Returning *via.RedirectError or *via.ToastError from an action handler to trigger a side effect was clever but conflated two concepts: - Go errors mean "this failed." - via.Redirect / via.Toast meant "this succeeded, and also do this." Readers had to know the framework's special sentinel-unwrap to interpret the return — and the imperative form (ctx.Redirect, ctx.Toast) already did the same thing without smuggling intent through the error channel. Removed: - intents.go (RedirectError + Redirect + ToastError + Toast) - intents_test.go - The sentinel-unwrap branch in runAction Sites that returned the sentinels now use ctx.Redirect / ctx.Toast imperatively before `return nil`. README updated. Net effect: one less concept to learn. Errors mean errors; side effects go through ctx methods.
Task 7 of the original DX punch list.
The framework discovers OnInit / OnConnect / OnDispose by method-name
reflection at Mount, so no public interface was required. But that
meant the lifecycle hooks were invisible to Go tooling — users had to
read the README to learn they existed and what their signatures were.
Declared three optional interfaces alongside Composition:
type Initializer interface { OnInit(ctx *Ctx) error }
type Connector interface { OnConnect(ctx *Ctx) error }
type Disposer interface { OnDispose(ctx *Ctx) }
Satisfying them is not required — Mount still works on the same
reflection path. But declaring them on a composition:
- documents the hook signature inline,
- makes hooks discoverable via `go doc`, IDE jump-to-interface,
and stdlib-style "implements" exploration,
- lets the compiler catch signature drift earlier (a stray return
type or extra parameter becomes a "does not satisfy" error).
Naming follows Go's one-method-per-interface convention
(io.Reader → Read, fmt.Stringer → String).
ExecScriptf duplicated ExecScript(fmt.Sprintf(...)); Ticker.Paused was an unused observation on a controller; Page existed only for editor autocomplete of optional hooks already documented by the Initializer / Connector / Disposer interfaces. Co-Authored-By: Claude Opus 4.7 <[email protected]>
…tate Quick Start no longer compiles against current API: View takes *CtxR, StateTab.Text takes ctx, and the typed shape handles (StateTabNum / SignalNum) match the real counter example. Reactive-state prose still referred to Set after the Set→Write rename, and Security still pointed at RotateSession after the move to sess.Rotate.
Lead with what Via is and who it's for. Promote honesty sections (What Via is NOT, Restart and tab survivability) above the fold so production constraints are visible before a reader invests. Compress the four-reactive-shapes reference middle; escape-hatch trivia moves to godoc.
Replaces a timing guess (time.Sleep(20 * time.Millisecond)) with a deterministic wait for the SSE goroutine to enter its select loop. runSSEStream now writes a `: ready\n\n` SSE comment immediately after OnConnect and the backlog drain, just before entering the select loop. SSE comments are silently ignored by Datastar (and any conformant client) per spec, so this adds no event surface for real consumers — it only gives the test harness a deterministic signal that an action POST won't race the handshake. Adds vt.Client.SSEReady and a matching rawTabClient.OpenSSEReady in form_test that open the stream and consume the comment off the channel before returning. All 49 call sites of the `tc.SSE() + time.Sleep(20*time.Millisecond)` idiom across 12 test files migrate to the new helpers, eliminating the suite's last source of wall-clock-timing brittleness on slow CI runners.
Six verbs previously had 0% statement coverage. Extends the existing shapePage + TestShape_NumOps / TestShape_SliceOps scaffolding with matching action methods and SSE-frame assertions, following the same behavioral-through-the-public-API pattern as the surrounding tests. Also migrates this file's SSE handshake waits to vt.Client.SSEReady, landing alongside the suite-wide migration in the preceding commit.
AccessLog and Recover scrub CR/LF from r.Method, r.URL.Path, and rid before logging; defaultLogger scrubs the assembled line. Closes CodeQL alerts #3-#7 (go/log-injection). File.Save now propagates os.File.Close errors via named return so a flush failure on the writable path is surfaced instead of silently dropped. Closes CodeQL alert #8 (go/unhandled-writable-file-close). Adds 3 regression tests in middleware_test.go covering forged path, forged rid, and forged method on the panic path.
Restyles the counterscope example to match the rest of the demos: pico header + nav with a brand link, a two-column grid for the Local/Shared cards (HGroup + Article + Footer with full-width +1 buttons), big tabular-num counters, centered footer. Adds an upright lightning-bolt logo (closed path, currentColor stroke, square caps) inline as viaLogo and saves the same SVG at repo root as logo.svg for reuse elsewhere.
…e-1.0 wording - Embed logo.svg as a centered hero above the badges. - Document mw.AccessLog/Recover CR/LF scrub (CWE-117) and the RedirectHTTPS vs RedirectHTTPSStrict split. - Rephrase the pre-1.0 NOT-bullet so it parses cleanly.
Reorganize plugins/echarts into concern-named files (plugin.go, chart.go, runtime.go, series.go, doc.go) with paired tests; all under CONVENTIONS' 300-line guideline. Coverage at 99.0%. Safety hardening: - SetOption / SetSeries / AppendData return marshal errors instead of silently emitting _c.setOption(null) - WithInitialOption validates input eagerly; mustJSON panics on internal marshal failure (was returning "null") - WithElementID / WithClass panic on whitespace; WithVersion / WithSource panic on empty strings - window.__viaCharts registry replaces per-chart `var echart_N` / `var echart_ro_N` window globals - ResizeObserver callback guards against null.resize() after Dispose - SetTheme preserves WithGroup linkage across the dispose+reinit Chart options for streaming dashboards: WithTimeAxis, WithYAxisRange, WithYAxisFormat, WithRightYAxis, WithDataZoom, WithCrosshair, WithCompactGrid, WithPalette, WithLegend, plus WithGroup, WithClass, WithSource, WithInitialOption. Series helpers: Pie, Scatter, Heatmap, LineDense, LineAreaDense, ScatterDense. Composable SeriesOption pattern with Dense, Filled, Stacked, Color, LineWidth, Symbol, Smoothed, Stepped, EndLabel, ConnectNulls, Progressive, Silent, YAxisIndex, MarkLine, MarkArea, Field (escape hatch). Utilities: Points (zip parallel slices), Tail (sliding-window trim). Runtime methods: SetTitle, SetSubtitle, SetLegend, SetYAxisRange, SetXAxisRange, SetTheme, SetLoading, Resize, Clear, Dispose, AppendPoint, AppendXY, AppendXYAt. Package doc rewritten around the dense-time-series story as a worked example; runnable godoc examples in example_test.go. sysmon modernized: dropped local timeAxisOpt / lineSeries helpers in favour of WithTimeAxis + WithCompactGrid + WithYAxisRange / WithYAxisFormat at chart construction, plus LineDense in the stream callback. OnConnect's per-chart SetOption setup pass removed — config now baked at OnInit time.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Typed reactive surface
Signal[T],State[T]split intoStateTab/StateSess/StateApp,Mount[C],DecodeForm[T],GetSess[T], typed query paramsOp(ctx)chains;Update(ctx, fn)transform APIStateSess/StateAppwrites auto fan-out to subscribed tabsApp.Broadcast/App.BroadcastSignalsfor cross-tab pushesInitializer/Connector/Disposerinterfaces;Stream(ctx, interval, fn)helperProduction middleware & server hardening
RequestID,AccessLog,Recover,HSTS,RedirectHTTPS,StrictCSP,RotateSession,WithMaxContexts,DefaultsWithReadTimeout/WriteTimeout/IdleTimeout/ReadHeaderTimeout,App.HTTPServer()factoryWithNotFoundcustom 404, panic at registration on route conflictsHandleStaticforembed.FS-backed assetsLoggerinterface +SlogLoggeradapter;via.Log(ctx)threadsvia_tab+ridSub-packages
via/sess— typed sessions (PutSess,GetSess[T],ClearSess)via/mw— pre-built middlewarevia/on— event bindings (Debounce,Throttle,Prevent,Stop,Key,SetSignal)via/vt— name-addressed test client;NewCtxfor direct action unit testsinternal/spec— action-trigger plumbinghpackageEach/EachIndexed/When/Group/Fragment/Switch/Case/Default/Classes/ClassMap/IfStrPlugins ported to typed API with tests:
picocss,echartsEcharts dense time-series toolkit (split into
plugin.go/chart.go/runtime.go/series.go/doc.gowith paired tests; 99.0% coverage)SetOption/SetSeries/AppendData; eagerWithInitialOptionvalidation;mustJSONstrict-panic; whitespace + empty-string panics onWithElementID/WithClass/WithVersion/WithSource;window.__viaChartsregistry replacing per-chartvarglobals;ResizeObservercleanup on Dispose;SetThemepreservesWithGrouplinkageWithTimeAxis,WithYAxisRange,WithYAxisFormat,WithRightYAxis,WithDataZoom,WithCrosshair,WithCompactGrid,WithPalette,WithLegend,WithGroup,WithClass,WithSource,WithInitialOptionPie,Scatter,Heatmap,LineDense,LineAreaDense,ScatterDense, plus composableSeriesOptionpattern (Dense,Filled,Stacked,Color,LineWidth,Symbol,Smoothed,Stepped,EndLabel,ConnectNulls,Progressive,Silent,YAxisIndex,MarkLine,MarkArea,Fieldescape hatch). Utilities:Points,TailSetTitle,SetSubtitle,SetLegend,SetYAxisRange,SetXAxisRange,SetTheme,SetLoading,Resize,Clear,Dispose,AppendPoint,AppendXY,AppendXYAtexample_test.goExamples: counter, countercomp, counterscope, greeter, pathparams, picocss, sysmon (modernized to use the new echarts toolkit), auth (typed sessions), plus new
todos/feed/uploadPerf
reflect.ValueOfperCtxIntrospection:
App.Routes(),App.Compositions(),App.LiveTabs()Runtime split into focused files (broadcast, push, server, document, encoding, info, intents, ctx, sse, stream, walker, file, form, csp, log, …)
Test plan
ci-check.shgreen: gofmt, go vet, build all packages, build all 11 examples,go test, alloc gatesgo test -race ./...cleanCounterRender≤180,CounterAction≤149,CounterActionWithLogger≤149Defaults+RotateSession+HSTS+RedirectHTTPS+StrictCSP)