Middleware Model
The onion-model middleware pattern used for intercepting and transforming kernel operations.
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 gives caching, transformation, and cross-cutting concerns a single home outside kernel code.
Context and Motivation
Kernel operations (describe, evaluate, render, export) 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 composes such behavior declaratively at one interception point -- and because the model is an onion, even short-circuited results flow back through outer layers for consistent post-processing.
How It Works
A middleware defines wrap-style hooks: wrapDescribe, wrapEvaluate, wrapRender, and wrapExport. A hook receives (input, next, services) and calls next(input) to continue the chain. Code before next(input) runs on the way down; code after runs on the way back up.
The hooks wrap parameter description, evaluation, selected-view rendering, and selected-export writing respectively. Implement any subset; an absent hook passes through.
Parameters at the evaluation boundary
An evaluation request's parameters are caller overrides. Views and exports project its retained handle. A unit-bound numeric field accepts either a number in the unit declared by the kernel or unit-bearing text such as "20 in". Middleware may place stored values below those overrides, but it does not convert units or fill kernel defaults. The runtime converts unit-bearing text and fills defaults once, after the evaluation middleware chain and immediately before calling the kernel.
The resulting precedence is: kernel defaults, then stored values, then caller overrides.
Middleware dependencies
resolve declares extra watched paths independently of wrap hooks. Every declaration includes the operations it invalidates:
import { defineMiddleware } from '@taucad/runtime/middleware';
export const metadataFiles = defineMiddleware({
id: 'metadata-files',
name: 'Metadata files',
resolve({ entryPath }) {
return [{ path: `${entryPath}.metadata.json`, affects: ['evaluate'] }];
},
});Valid operations are describe, evaluate, render, and export. The runtime hashes and watches a declaration only for the listed operations; it never infers dependency scope from the middleware's wrap hooks.
KernelMiddlewareServices
Every hook receives a KernelMiddlewareServices as its third argument:
signal-- operation-scoped cancellation signal, shared with the active kernel calllogger-- logger with the middleware name pre-configuredtracer-- span tracer for middleware-authored instrumentationfilesystem-- runtime filesystem for file I/O such as cache filescompute-- runtime-owned reuse capability; it may be offstate-- type-safeMiddlewareState<T>with.valueand.update(partial), validated againststateSchemaoptions-- resolved options:optionsSchemadefaults merged with caller overridesdependencies-- read-onlyDependencyarray for the current operationdependencyHash-- pre-computed SHA-256 over all dependencies, ready to use as a cache key
State and options schemas
A middleware that persists data during an operation declares stateSchema (a Zod object schema). The framework creates a fresh state per operation and validates every update against the schema. State is scoped to one operation; it does not survive across renders. Options follow the same pattern through optionsSchema.
Key Relationships
- Ordering: first registered is outermost. Put caches early so they wrap expensive computation.
- Parameter values: evaluation middleware receives caller overrides and outer middleware values; defaults arrive at the kernel boundary.
Implications
- Short-circuiting -- a cache hit can return without calling
next(input). The result still flows through outer post-processing, so coordinate transforms and edge detection apply to cached results too. - No shared mutable state -- state is per-operation. Cross-operation reuse belongs to the runtime-managed cache or a declared compute capability.
- Error handling -- a throwing middleware is caught by the framework and surfaced as a structured error; outer post-processing does not run on 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