TAU/ DOCS

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 call
  • logger -- logger with the middleware name pre-configured
  • tracer -- span tracer for middleware-authored instrumentation
  • filesystem -- runtime filesystem for file I/O such as cache files
  • compute -- runtime-owned reuse capability; it may be off
  • state -- type-safe MiddlewareState<T> 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

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

On this page