Repository navigation
Conversation
psteinroe
force-pushed
the
feat/middleware
branch
from
October 2, 2026 23:38
b880993 to
88ae6a1
Compare
This branch has not been deployed
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
Conductor.create({ middleware: [...] }): functions that run around every attempt of a task handler, including each resume afterctx.sleep(),ctx.waitForEvent()andctx.invoke(). The staticcontextcan't rebuild live objects such as clients or thread handles from execution data on each start and resume. Middleware can.Typing
Keys passed to
nextare inferred into the handlerctxtype (a reverse-mapped tuple over the middleware array, like Inngest'stransformInput). Middleware runs in array order and sees earlier additions at runtime. Its ownctxparameter is typed with the base context only.nextreturns an opaqueMiddlewareResult, so a middleware must return whatnextreturned.Middleware,MiddlewareExecutionandMiddlewareResultare exported.execution,metadataandresumedexecutionexposes:id,task_key,queue,attemptandparent_execution_id.metadata: the same value asctx.metadatafrom feat(executions): typed, propagated execution metadata #72, typed by the Conductor's metadata schema andundefinedwhen the execution has none.resumed: true when earlier attempts left durable progress. That means a persisted step (including sleeps, received events and child results), a pending event wait (execution_waitsubscription), or a pending child invocation (waiting_on_execution_id).These values come from the claim query, which now also returns
attemptsandparent_execution_id. Computingresumedadds two indexedexistsprobes per claimed row. The root id is not included because no column stores it.Abort safety
Hang-ups don't reject:
abortAndHangupaborts and returns a promise that never settles. So thattry/finallycleanup still runs,next()resolves when the attempt suspends, is cancelled or times out, and rejects only when the handler throws. The worker races the whole chain against the abort promise, and that race always settles first. Whatever middleware returns, throws or swallows after an abort is therefore ignored. Middleware errors fail the attempt and it is retried, the same as handler errors.Scope
Middleware is Conductor-level only. Batch tasks skip it, the same way they already receive no custom context: their context has no steps and covers many executions. The internal maintenance and event-dispatch tasks skip it too.
Verification
tests/integration/middleware.test.ts:resumed, parent and propagatedmetadatavaluesAsyncLocalStorageandfinallywrapping works on suspension and on completiontests/unit/middleware-types.test.ts: chained additions,metadatatyped by the schema, middleware declared separately, no middleware, and that middleware must return whatnextreturnedjust lint,bun run typecheckand the fullbun testpass (409 tests)Follow-ups / out of scope
ctx.invoke(out of scope): resuming afterctx.invokeincrementsattempts.updated_invoke_parentsdoesn't decrement it, while released and event-wait suspensions do. The parent'sexecution.attemptreads 2 after one child resume, and every child invocation uses up one of the parent'smaxAttempts. This looks like a bug and is not fixed here.