feat: add token-bucket rate limiting for action endpoints

Add per-context and per-action rate limiting using golang.org/x/time/rate.
Configure globally via Options.ActionRateLimit or per-action with
WithRateLimit(). Defaults to 10 req/s with burst of 20.
This commit is contained in:
Ryan Hamamura
2026-02-06 11:52:07 -10:00
parent b7acfa6302
commit 0762ddbbc2
6 changed files with 190 additions and 13 deletions

View File

@@ -12,6 +12,7 @@ import (
"time"
"github.com/ryanhamamura/via/h"
"golang.org/x/time/rate"
)
// Context is the living bridge between Go and the browser.
@@ -27,7 +28,8 @@ type Context struct {
componentRegistry map[string]*Context
parentPageCtx *Context
patchChan chan patch
actionRegistry map[string]func()
actionLimiter *rate.Limiter
actionRegistry map[string]actionEntry
signals *sync.Map
mu sync.RWMutex
ctxDisposedChan chan struct{}
@@ -104,26 +106,31 @@ func (c *Context) isComponent() bool {
// h.Button(h.Text("Increment n"), increment.OnClick()),
// )
// })
func (c *Context) Action(f func()) *actionTrigger {
func (c *Context) Action(f func(), opts ...ActionOption) *actionTrigger {
id := genRandID()
if f == nil {
c.app.logErr(c, "failed to bind action '%s' to context: nil func", id)
return nil
}
entry := actionEntry{fn: f}
for _, opt := range opts {
opt(&entry)
}
if c.isComponent() {
c.parentPageCtx.actionRegistry[id] = f
c.parentPageCtx.actionRegistry[id] = entry
} else {
c.actionRegistry[id] = f
c.actionRegistry[id] = entry
}
return &actionTrigger{id}
}
func (c *Context) getActionFn(id string) (func(), error) {
if f, ok := c.actionRegistry[id]; ok {
return f, nil
func (c *Context) getAction(id string) (actionEntry, error) {
if e, ok := c.actionRegistry[id]; ok {
return e, nil
}
return nil, fmt.Errorf("action '%s' not found", id)
return actionEntry{}, fmt.Errorf("action '%s' not found", id)
}
// OnInterval starts a go routine that sets a time.Ticker with the given duration and executes
@@ -482,7 +489,8 @@ func newContext(id string, route string, v *V) *Context {
routeParams: make(map[string]string),
app: v,
componentRegistry: make(map[string]*Context),
actionRegistry: make(map[string]func()),
actionLimiter: newLimiter(v.actionRateLimit, defaultActionRate, defaultActionBurst),
actionRegistry: make(map[string]actionEntry),
signals: new(sync.Map),
patchChan: make(chan patch, 1),
ctxDisposedChan: make(chan struct{}, 1),