Cooperate with Cancellation
Use the operation-scoped AbortSignal in middleware, kernels, dependency hooks, and custom bundlers.
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
- Destructure
signalfrom 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
signalto native cancellable APIs such asfetch. - 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
defineMiddleware keeps its (input, handler, runtime) signature; signal is inferred on the third argument:
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
resolve receives MiddlewareDependencyServices as its second argument, with options beside signal, filesystem, and logger:
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
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:
type CancellableEngine = {
cancel(): void;
render(input: unknown): Promise<unknown>;
};
export const renderWithCancellation = async (
engine: CancellableEngine,
signal: AbortSignal,
input: unknown,
): Promise<unknown> => {
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
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 for the full author contract.
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.