middleware: new middleware package built on top of state#90
Open
ecordell wants to merge 1 commit into
Open
Conversation
ecordell
force-pushed
the
middleware2
branch
2 times, most recently
from
July 17, 2026 15:12
6a79f89 to
1de2fa1
Compare
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.
Adds a middleware layer to the
statepipeline and a newstate/middlewarepackage with a couple of ready-made implementations.Middleware
Middleware(func(NewStep) NewStep) with an ambient registration mechanism:WithAmbientMiddlewarecomposes middleware into context (first-registered outermost), andAmbientDispatch(steps...)opts a pipeline in, firing the ambient middleware once per listed step. Middleware registered mid-pipeline applies to subsequent steps, and granularity is explicit: pass steps individually for per-step firing, or a composite to wrap it as a unit.MakeMiddleware(before, after)builds middleware from hook pairs with bracket semantics: the after-hook is guaranteed to fire on every outcome the program survives: continue, terminal (queue.Done), cancellation, or a recovered panic.middleware.Recovermarks its dynamic extent viastate.WithPanicRecovery; brackets inside a marked scope close during panic unwinding (without touching the panic), so spans/timers release before the error handler runs. Outside a recovery scope, panics propagate untouched with no hooks fired.Parallelclears the marker for branches since recovery can't cross goroutines.Named(name, step)/StepName(ctx)annotate steps for observability, with a race-safe, outermost-wins capture slot (WithStepNameCapture/CapturedStepName) so middleware can read names even from terminal steps.state/middlewareLog(logger)logs each step's name and duration via slog.Recovermoved fromstate; wraps a step with panic recovery, routing a*state.PanicErrorthrough the error handler.API changes
state.Recover→middleware.Recover;ParallelWith(wrapper, steps...)→Parallel(Map(wrapper, steps...)...).queue.Done/queue.Requeueare now package vars (usequeue.Done, notqueue.Done());queue.OnErrorremoved, makes sense to branch on context directly.ctxkeybumped to taggedv0.1.0.Example
Ambient middleware is registered on the context with
WithAmbientMiddlewareandapplied by wrapping the pipeline in
AmbientDispatch. Middleware fires once perstep.
AmbientDispatchis the opt-in point, so pipelines that don't use it arecompletely unaffected.
With
middleware.Log, each step produces a log line like:Middleware can also be registered mid-pipeline; it applies to all subsequent
steps in the same
AmbientDispatchcall:middleware.Recoveris the exception — it's applied per-step rather thanambiently, for steps that may panic: