(fix): webhook secret constant-time compare
Golang lint / lint (pull_request) Successful in 2m53s
Golang lint / lint (push) Successful in 2m55s

(fix): observer rename, panic emits ErrorEvent
(refactor): Runner.Once removed, Scene.PluginName unexported
(doc): godoc hygiene pre-1.0.0
This commit is contained in:
2026-05-20 12:06:59 +03:00
parent 1e26d871b5
commit 61d0b1ebb8
29 changed files with 191 additions and 138 deletions
+11 -20
View File
@@ -16,20 +16,19 @@ type RunnerFn[T AppData] func(*Bot[T]) error
// Once Execute() is called, the Runner should not be modified.
//
// Execution semantics:
// - once=true, async=false: Run once synchronously (blocks).
// - once=true, async=true: Run once in a goroutine (non-blocking).
// - once=false, async=true: Run repeatedly in a goroutine with timeout.
// - once=false, async=false: Invalid configuration — ignored with warning.
// - every=0, async=false: Run once synchronously (blocks).
// - every=0, async=true: Run once in a goroutine (non-blocking).
// - every>0, async=true: Run repeatedly in a goroutine with timeout.
// - every>0, async=false: Invalid configuration — ignored with warning.
type Runner[T AppData] struct {
name string // Human-readable name for logging
once bool // If true, runs once; if false, runs periodically
async bool // If true, runs in a goroutine; else, runs synchronously
every time.Duration // Duration to wait between periodic executions (ignored if once=true)
fn RunnerFn[T] // The function to execute
}
// NewRunner creates a new Runner with the given name and function.
// By default, the Runner is configured as async=true (non-blocking).
// By default, the Runner is configured as async=true (non-blocking), once=true/mo
//
// Builder methods (Once, Async, Every) can be chained to customize behavior.
// DO NOT call builder methods concurrently or after Execute().
@@ -38,18 +37,10 @@ func NewRunner[T AppData](name string, fn RunnerFn[T]) Runner[T] {
name: name,
fn: fn,
async: true, // Default: run asynchronously
every: 0, // Default: no timeout (ignored if once=true)
every: 0, // Default: 0 - one time
}
}
// Once sets whether the runner executes once or repeatedly.
// If true, the runner runs only once.
// If false, the runner runs in a loop with the configured timeout.
func (r Runner[T]) Once(once bool) Runner[T] {
r.once = once
return r
}
// Async sets whether the runner executes synchronously or asynchronously.
// If true, the runner runs in a goroutine (non-blocking).
// If false, the runner blocks the caller during execution.
@@ -94,16 +85,16 @@ func (bot *Bot[T]) ExecRunners(ctx context.Context) {
bot.logger.Infoln("Executing runners...")
for _, runner := range bot.runners {
// Validate configuration
if !runner.once && !runner.async {
if runner.every > 0 && !runner.async {
bot.logger.Warnf("Runner %s not once, but sync — skipping\n", runner.name)
continue
}
if !runner.once && runner.async && runner.every == 0 {
if runner.every > 0 && runner.async && runner.every == 0 {
bot.logger.Warnf("Background runner \"%s\" has no timeout — skipping\n", runner.name)
continue
}
if runner.once && runner.async {
if runner.every == 0 && runner.async {
// One-time async: fire and forget
bot.runnerOnceWG.Add(1)
go func(r Runner[T]) {
@@ -126,7 +117,7 @@ func (bot *Bot[T]) ExecRunners(ctx context.Context) {
bot.logger.Warnf("Runner %s failed: %s\n", r.name, err)
}
}(runner)
} else if runner.once && !runner.async {
} else if runner.every == 0 && !runner.async {
// One-time sync: block until done
t := time.Now()
err := runner.fn(bot)
@@ -149,7 +140,7 @@ func (bot *Bot[T]) ExecRunners(ctx context.Context) {
if elapsed > time.Second*2 {
bot.logger.Warnf("Runner %s too slow. Elapsed time %v >= 2s\n", runner.name, elapsed)
}
} else if !runner.once && runner.async {
} else if runner.every > 0 && runner.async {
// Background loop: periodic execution with graceful shutdown
bot.runnerBgWG.Add(1)
go func(r Runner[T]) {