Middleware Model
The onion model middleware pattern used for intercepting and transforming kernel operations.
Middleware Model
Middleware in @taucad/runtime uses the onion model: each layer wraps the next, and code after the inner call runs on the return journey. This pattern enables caching, transformation, and cross-cutting concerns without modifying kernel code.
Context and Motivation
Kernel operations (getParameters, createGeometry, exportGeometry) often need cross-cutting behavior: caching results, transforming coordinates, adding edge data for rendering. Hard-coding these into each kernel would duplicate logic and couple kernels to UI concerns. Middleware provides a single interception point where such behavior can be composed declaratively. The onion model ensures that even short-circuited results flow through upstream middleware for consistent post-processing.
How It Works
Each middleware can define wrap-style hooks: wrapGetParameters, wrapCreateGeometry, and wrapExportGeometry. A hook receives (input, handler, runtime) and must call handler(input) to continue the chain. Code before handler() runs on the way down; code after runs on the way back up.
Execution order for [A, B, C] with A outermost:
- A pre -> B pre -> C pre -> kernel
- kernel returns
- C post -> B post -> A post -> result to caller
wrapCreateGeometry, wrapGetParameters, wrapExportGeometry
The three hooks mirror the three kernel operations:
- wrapCreateGeometry -- Wraps geometry computation. Use for caching (short-circuit on hit), coordinate transforms, or edge detection. Receives
CreateGeometryInput; returnsCreateGeometryResult. - wrapGetParameters -- Wraps parameter extraction. Use for parameter caching or schema transformation. Receives
GetParametersInput; returnsGetParametersResult. - wrapExportGeometry -- Wraps export. Use for format conversion or post-processing of export blobs. Receives
ExportGeometryInput; returnsExportGeometryResult.
A middleware can implement any subset. Unimplemented hooks are skipped; the chain passes through directly to the kernel (or the next middleware that implements the hook).
KernelMiddlewareRuntime
Each wrap hook receives a KernelMiddlewareRuntime as the third argument, providing:
logger-- Logger with the middleware name pre-configuredfilesystem--RuntimeFileSystemfor file I/O (e.g., reading/writing cache files)state-- Type-safeMiddlewareState<T>with.valueand.update(partial), validated againststateSchemaoptions-- Parsed options fromoptionsSchemadependencies-- Read-only array ofDependencyobjects for the current operationdependencyHash-- SHA-256 hash of all dependencies, useful as a cache key
State Management with Zod stateSchema
Middleware that needs to persist data during an operation can define stateSchema (a Zod object schema). The framework creates a state object per operation with state.value and state.update(partial). Updates are validated against the schema. State is scoped to a single operation; it does not persist across renders.
Comparison to Express/Koa Middleware
Express and Koa use a similar pattern: (req, res, next) => { ...; next(); ... }. The @taucad/runtime model differs in two ways:
- Wrap style -- Instead of
next(), you receivehandlerand callhandler(input). The return value flows back through the chain. This makes async composition and result transformation natural. - Typed input/output -- Each hook has a specific input and output type. No generic request/response object; the types reflect the operation.
Key Relationships
- Middleware and Kernel: Middleware never calls the kernel directly. It calls
handler(input), which eventually reaches the kernel. The kernel is unaware of middleware. - Middleware and Runtime: Each middleware receives a
KernelMiddlewareRuntimewith the services listed above. - Middleware Order: Registration order determines wrapping order. First registered = outermost. For caching, put caches early (outer) so they wrap the expensive inner computation.
Implications
- Short-circuiting -- A cache hit can return without calling
handler(). The result still flows through upstream middleware post-processing, so coordinate transforms and edge detection apply to cached results too. - No shared mutable state -- State is per-operation. For cross-operation caches, use the filesystem or an external store.
- Error handling -- If middleware throws, the framework catches it and returns a structured error. Upstream middleware does not run post-processing for that path.
Further Reading
- Architecture -- Where middleware sits in the stack
- Plugin System -- How middleware plugins register
- Use Middleware -- Add built-in middleware to your client
- Create Custom Middleware -- Implement middleware with
defineMiddleware - API: Middleware --
defineMiddlewareand wrap hook types