# Middleware Model URL: /runtime/concepts/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 gives caching, transformation, and cross-cutting concerns a single home outside kernel code. ## Context and Motivation [#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 [#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. ```mermaid flowchart TB subgraph Request["Request Journey (outside-in)"] R1["Middleware A: pre"] R2["Middleware B: pre"] R3["Middleware C: pre"] R4["Kernel"] end subgraph Response["Response Journey (inside-out)"] S4["Kernel"] S3["Middleware C: post"] S2["Middleware B: post"] S1["Middleware A: post"] end R1 --> R2 R2 --> R3 R3 --> R4 R4 --> S4 S4 --> S3 S3 --> S2 S2 --> S1 ``` 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 [#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 [#middleware-dependencies] `resolve` declares extra watched paths independently of wrap hooks. Every declaration includes the operations it invalidates: ```typescript 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 [#kernelmiddlewareservices] Every hook receives a `KernelMiddlewareServices` as its third argument: * `signal` -- operation-scoped cancellation signal, shared with the active kernel call * `logger` -- logger with the middleware name pre-configured * `tracer` -- span tracer for middleware-authored instrumentation * `filesystem` -- [runtime filesystem](/runtime/api/filesystem) for file I/O such as cache files * `compute` -- runtime-owned reuse capability; it may be off * `state` -- type-safe `MiddlewareState` with `.value` and `.update(partial)`, validated against `stateSchema` * `options` -- resolved options: `optionsSchema` defaults merged with caller overrides * `dependencies` -- read-only `Dependency` array for the current operation * `dependencyHash` -- pre-computed SHA-256 over all dependencies, ready to use as a cache key ### State and options schemas [#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 [#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 [#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 [#further-reading] * [Architecture](/runtime/concepts/architecture) -- where middleware sits in the stack * [Plugin System](/runtime/concepts/plugin-system) -- how middleware plugins register * [Use Middleware](/runtime/guides/using-middleware) -- add built-in middleware to your client * [Create Custom Middleware](/runtime/guides/custom-middleware) -- implement middleware with `defineMiddleware` * [API: Middleware](/runtime/api/middleware) -- `defineMiddleware` and wrap hook types