# Cooperate with Cancellation URL: /runtime/guides/cooperate-with-cancellation Every operation-scoped plugin runtime exposes the platform `AbortSignal`. Use it to stop async work once the worker receives supersession or timeout cancellation, including on transports without `SharedArrayBuffer`. The signal is fresh for each operation. The kernel, middleware, dependency-resolution, and bundler work in that operation share it; the next render gets a new one. ## Rules [#rules] * Destructure `signal` from the existing runtime argument. Do not add another hook parameter or plugin option. * Call `signal.throwIfAborted()` before and after work that cannot accept a signal directly. * Pass `signal` to native cancellable APIs such as `fetch`. * Do not retain the signal, an abort listener, or the operation runtime for later work. * Remove manually registered listeners in `finally`. * Do not use `Promise.race()` to report cancellation while state-mutating work continues in the background. ## Middleware wrap hooks [#middleware-wrap-hooks] `defineMiddleware` keeps its `(input, handler, runtime)` signature; `signal` is inferred on the third argument: ```typescript import { defineMiddleware } from '@taucad/runtime/middleware'; export const calibrationMiddleware = defineMiddleware({ id: 'calibration', name: 'Calibration', async wrapEvaluate(input, handler, { signal }) { signal.throwIfAborted(); const response = await fetch(`/api/calibration?entry=${encodeURIComponent(input.entryPath)}`, { signal, }); if (!response.ok) { throw new Error(`Calibration request failed with HTTP ${response.status}. Check the calibration service.`); } const scale: unknown = await response.json(); if (typeof scale !== 'number' || !Number.isFinite(scale)) { throw new TypeError('Calibration service must return a finite numeric scale.'); } signal.throwIfAborted(); return handler({ ...input, parameters: { ...input.parameters, scale }, }); }, }); ``` `handler(input)` continues the onion chain with the same operation signal. Code after `handler()` still runs on the return journey unless the handler throws cancellation. ## Middleware dependency hooks [#middleware-dependency-hooks] `resolve` receives `MiddlewareDependencyServices` as its second argument, with `options` beside `signal`, `filesystem`, and `logger`: ```typescript import { defineMiddleware } from '@taucad/runtime/middleware'; import { z } from 'zod'; export const calibrationFiles = defineMiddleware({ id: 'calibration-files', name: 'Calibration files', optionsSchema: z.object({ directory: z.string().default('.tau/calibration'), }), async resolve({ entryPath }, { filesystem, options, signal }) { signal.throwIfAborted(); const path = `${options.directory}/${entryPath}.json`; await filesystem.exists(path); signal.throwIfAborted(); return [{ path, affects: ['evaluate'], watchDebounce: 0 }]; }, }); ``` The runtime performs its own cancellation checks around filesystem boundaries. Explicit checks remain useful when a hook performs several dependent reads or transforms between those boundaries. ## Kernel methods [#kernel-methods] `KernelServices.signal` is the same operation signal middleware sees. Pass it to the engine's native cancellation API when one exists — `fetch(url, { signal })` for a remote engine, for example. When an engine exposes a callback-style `cancel()` instead, adapt it with a once-only listener and remove the listener in `finally`: ```typescript type CancellableEngine = { cancel(): void; render(input: unknown): Promise; }; export const renderWithCancellation = async ( engine: CancellableEngine, signal: AbortSignal, input: unknown, ): Promise => { const cancel = (): void => engine.cancel(); signal.addEventListener('abort', cancel, { once: true }); try { return await engine.render(input); } finally { signal.removeEventListener('abort', cancel); } }; ``` The listener must belong to one operation. Never register it during persistent kernel initialization. ## Custom bundlers [#custom-bundlers] `detectImports`, `bundle`, and `execute` use `(input, services, context)`; `BundlerServices.signal` sits on the second argument. Initialization and `onDispose` are lifecycle-scoped rather than operation-scoped, so they receive no render signal — store durable bundler state in `context` and use only the per-call services for cancellation. See [Configure the Bundler](/runtime/guides/bundler-configuration) for the full author contract. ## What cancellation cannot do [#what-cancellation-cannot-do] An `AbortSignal` is cooperative. It cannot interrupt synchronous native or WASM work before execution returns to JavaScript. Isolated transports recover by terminating an unresponsive host after a bounded grace period; same-isolate transports reject non-zero timeout configuration because they cannot provide that guarantee. ## Related [#related] * [Configure Render Timeouts](/runtime/guides/render-timeouts) * [Create Custom Middleware](/runtime/guides/custom-middleware) * [Configure the Bundler](/runtime/guides/bundler-configuration) * [Create a Custom Kernel](/runtime/guides/custom-kernel) * [Middleware API](/runtime/api/middleware) * [Bundler API](/runtime/api/bundler)