feat: typed contract errors, activity middleware + context, contract-level activity defaults#298
Open
btravers wants to merge 1 commit into
Open
feat: typed contract errors, activity middleware + context, contract-level activity defaults#298btravers wants to merge 1 commit into
btravers wants to merge 1 commit into
Conversation
…level activity defaults Three features inspired by oRPC v2's contract-first design, adapted to Temporal's failure and options model: - Contract-declared typed domain errors: `errors` maps on defineActivity/defineWorkflow, typed constructors in activity helpers and workflow context, ApplicationFailure serialization (type = error name, details = validated data, nonRetryable from the contract), and typed rehydration on the workflow proxy and the client. - Activity middleware + typed dependency context on declareActivitiesHandler (createContext + contract-aware middleware chain operating on the AsyncResult inside the validation boundary). - Contract-level default ActivityOptions on defineActivity, merged as activityOptions < defaultOptions < activityOptionsByName. New `@temporal-contract/contract/errors` entry point keeps unthrown an optional peer of the root entry. Activities without declared errors keep the throwing Promise shape — backward compatible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR extends temporal-contract to make contracts a stronger source of truth for failure semantics and operational behavior by adding typed, schema-validated domain errors, a contract-aware activity handler pipeline (middleware + DI context), and contract-level default activity options.
Changes:
- Add contract-declared typed domain errors for activities and workflows, with worker-side serialization to
ApplicationFailureand workflow/client-side rehydration intoContractError. - Add
declareActivitiesHandlersupport for per-invocation typed context (createContext) and an outermost-first middleware chain that operates onAsyncResult. - Add contract-level per-activity
defaultOptionsand merge them into workflow activity proxies with documented precedence.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/worker/src/workflow.ts | Adds workflow-level typed error constructors (context.errors) and converts thrown ContractError into ApplicationFailure. |
| packages/worker/src/internal.ts | Merges contract defaultOptions into activity proxy options with precedence over workflow-wide defaults. |
| packages/worker/src/activity.ts | Adds typed contract errors for activities, per-invocation context injection, and middleware support inside the validation boundary. |
| packages/worker/src/activities-proxy.ts | Changes workflow-side activity calls to return AsyncResult for errors-declaring activities and rehydrates typed errors. |
| packages/worker/src/errors.ts | Introduces ContractErrorDataValidationError plus ActivityError/ActivityCancelledError for classification. |
| packages/worker/src/contract-errors.ts | Adds internal bridge converting ContractError → ApplicationFailure with schema validation and contract retry semantics. |
| packages/worker/src/workflow-proxy.spec.ts | Adds tests for contract-level defaultOptions precedence/merging behavior. |
| packages/worker/src/workflow-errors.spec.ts | Adds runtime coverage for workflow-level typed errors being converted to ApplicationFailure. |
| packages/worker/src/activity-contract-errors.spec.ts | Adds runtime coverage for activity typed errors, middleware behavior, and createContext. |
| packages/worker/src/activities-proxy.spec.ts | Adds runtime coverage for workflow-side activity error rehydration/classification. |
| packages/contract/src/types.ts | Adds ErrorDefinition, error payload inference helpers, and portable activity default option types. |
| packages/contract/src/builder.ts | Validates errors maps and strict defaultOptions at defineContract time. |
| packages/contract/src/index.ts | Exports new typed-error and default-options types from the root entry point. |
| packages/contract/src/errors.ts | Adds the @temporal-contract/contract/errors entry point with constructors + rehydration helpers. |
| packages/contract/src/errors.spec.ts | Adds unit tests for error constructor building and failure rehydration behavior. |
| packages/contract/src/builder.spec.ts | Adds tests covering acceptance/rejection of errors maps and defaultOptions. |
| packages/contract/package.json | Exposes ./errors export and includes it in build/dev scripts. |
| packages/client/src/internal.ts | Adds workflow-failure rehydration helper for workflow-level contract errors. |
| packages/client/src/client.ts | Surfaces workflow contract errors in typed client error unions; rehydrates matching failures. |
| packages/client/src/index.ts | Re-exports typed contract error surface (ContractError, unions) from the client entry point. |
| packages/client/src/client.spec.ts | Adds unit tests validating workflow-level typed error rehydration and fallback behavior. |
| docs/guide/defining-contracts.md | Documents typed errors and contract-level default activity options. |
| docs/guide/activity-handlers.md | Documents createContext, typed errors production, and the new middleware API. |
| docs/guide/worker-implementation.md | Documents workflow-side consumption of typed activity errors + option precedence. |
| docs/guide/client-usage.md | Documents typed workflow errors surfacing as ContractError on the typed client. |
| .changeset/orange-planets-learn.md | Adds a minor changeset describing the three new features across packages. |
| .agents/rules/handlers.md | Updates repo guidance to include helpers/middleware and workflow typed-error semantics. |
| .agents/rules/contract-patterns.md | Updates contract pattern guidance with errors and defaultOptions. |
Comment on lines
+408
to
+414
| const chain = middlewareChain.reduceRight<typeof invokeImplementation>( | ||
| (nextStage, mw) => (stageInput) => | ||
| mw({ ...info, input: stageInput, context: context as TContext }, (...override) => | ||
| nextStage(override.length > 0 ? override[0] : stageInput), | ||
| ), | ||
| invokeImplementation, | ||
| ); |
| ActivityOutputValidationError, | ||
| } from "./errors.js"; | ||
| import { contractErrorToApplicationFailure } from "./contract-errors.js"; | ||
| import { extractHandlerInput } from "./internal.js"; |
Comment on lines
+132
to
+136
| const contractDefaults = definition.defaultOptions; | ||
| const override = overrideByName[name]; | ||
| if (!contractDefaults && !override) { | ||
| continue; | ||
| } |
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
Three features inspired by an analysis of oRPC v2's contract-first design, adapted to Temporal's failure and options model. Closes the gap where domain failures collapsed into an opaque
ApplicationFailure/WorkflowFailedError.cause, and formalizes two patterns the docs previously showed as manual boilerplate (middleware, dependency injection).1. Contract-declared typed domain errors
defineActivity/defineWorkflowaccept anerrorsmap ({ data?: StandardSchema, message?, nonRetryable? }per name), validated atdefineContracttime.(args, { errors }) => Err(errors.PaymentDeclined({ reason })). The boundary validates the payload and throwsApplicationFailurewithtype= error name,details[0]= validated data, andnonRetryabledriven by the contract — retry semantics ship with the contract instead of being scattered per worker. Undeclared names / invalid payloads fail fast with the new terminalContractErrorDataValidationError.AsyncResult<Output, DeclaredUnion | ActivityError | ActivityCancelledError>, exactly mirroring the child-workflow API (cancellation discriminated first, Temporal'sActivityFailurewrapper unwrapped). Activities without declared errors keep the throwingPromiseshape — fully backward compatible.errors;throw context.errors.EmptyOrder({...})fails the execution as a typedApplicationFailure, and the client rehydrates matching failures intoContractErroronexecuteWorkflow/handle.result()(falling back toWorkflowFailedErroron type/payload mismatch, so contract drift degrades to today's untyped behavior instead of a wrong typed error).@temporal-contract/contract/errorsentry point (same pattern as./result-async) keepsunthrownan optional peer of the root entry.2. Activity middleware + typed dependency context
declareActivitiesHandlergains:createContext— typed DI, invoked per execution with{ activityName, workflowName }, surfaced to implementations and middleware ashelpers.context.middleware— contract-aware chain, outermost-first, running inside the validation boundary and operating on the unthrownAsyncResult(observes modeled failures on theerrchannel, can short-circuit, can substitute input).3. Contract-level default activity options
defineActivity({ defaultOptions })carries timeouts/retry policy on the contract (structural types — the contract package keeps zero@temporalio/*deps; strict-object validation catches typos at definition time). Worker merge precedence per activity:activityOptions(workflow-wide) < contractdefaultOptions<activityOptionsByName. The one-extra-proxy-per-customized-activity optimization is preserved.Testing
builder.spec,workflow-proxy.spec,client.spec)pnpm build,typecheck,lint,knip,format --checkall cleandefining-contracts,activity-handlers,worker-implementation,client-usage,.agents/rules/*) and a minor changeset added for all three packagesRelated
Follow-up issues from the same analysis: #296 (contract introspection/diff tooling), #297 (
@temporal-contract/experimental-effect).Notes for review
datais validated on both producer and rehydration sides, matching the existing deliberate double-validation — same transform caveat as activity outputs.ValidationErrorsubclass rules respected:ContractErrorDataValidationErrorextends theApplicationFailure-basedValidationError(terminal, deterministic).🤖 Generated with Claude Code