Cancellation & Timeouts
Cancel any call with a standard AbortController: pass its signal in the call() setup object. A cancelled request comes back as request-aborted in response['@thrownErrors'], never in the route's typed error union.
import {initClient} from '@mionjs/client';
import type {MyApi} from './hello.routes.ts';
const {routes} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
const controller = new AbortController();
const resultPromise = routes.sayHello('John').call({signal: controller.signal});
// e.g. on component unmount or a user action
controller.abort();
const [greeting, , response] = await resultPromise;
if (response['@thrownErrors']?.[0].type === 'request-aborted')
console.log('Request was canceled');
else console.log(greeting);
import {createMionRouter, Routes} from '@mionjs/router';
const mion = createMionRouter();
const routes = {
sayHello: mion.route((ctx, name: string): string => `Hello ${name}`),
} satisfies Routes;
const myApi = mion.initRoutes(routes);
// the type the client uses, no server code ever reaches the browser
export type MyApi = typeof myApi;
request-aborted in your UI instead of showing an error.Per-Request Timeout
Set a timeout in milliseconds to cancel a request that takes too long. It comes back as request-timeout in response['@thrownErrors'].
import {initClient} from '@mionjs/client';
import type {MyApi} from './hello.routes.ts';
const {routes} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
// this request fails if it takes longer than 5 seconds
const [greeting, error, response] = await routes
.sayHello('John')
.call({timeout: 5000});
// transport failures never join the route's typed errors; each is an open RpcError<string>
if (response['@thrownErrors']?.[0].type === 'request-timeout')
console.log('Request took too long');
else if (!error) console.log(greeting);
import {createMionRouter, Routes} from '@mionjs/router';
const mion = createMionRouter();
const routes = {
sayHello: mion.route((ctx, name: string): string => `Hello ${name}`),
} satisfies Routes;
const myApi = mion.initRoutes(routes);
// the type the client uses, no server code ever reaches the browser
export type MyApi = typeof myApi;
Default Timeout
Set a default timeout for all requests in initClient. A per-request timeout overrides it.
import {initClient} from '@mionjs/client';
import type {MyApi} from './hello.routes.ts';
// every request times out after 10 seconds
const {routes} = initClient<MyApi>({
baseURL: 'http://localhost:3000',
timeout: 10_000,
});
// uses the 10s default timeout
const [greeting] = await routes.sayHello('John').call();
console.log(greeting);
// 2s for this call only
const [, , response] = await routes.sayHello('Jane').call({timeout: 2000});
if (response['@thrownErrors']?.[0].type === 'request-timeout')
console.log('too slow');
import {createMionRouter, Routes} from '@mionjs/router';
const mion = createMionRouter();
const routes = {
sayHello: mion.route((ctx, name: string): string => `Hello ${name}`),
} satisfies Routes;
const myApi = mion.initRoutes(routes);
// the type the client uses, no server code ever reaches the browser
export type MyApi = typeof myApi;
Global Abort
client.abort() cancels every in-flight request at once, for example when a single page app navigates away. New requests work normally afterwards.
import {initClient} from '@mionjs/client';
import type {MyApi} from './hello-sum.routes.ts';
const {client, routes} = initClient<MyApi>({
baseURL: 'http://localhost:3000',
});
const p1 = routes.sayHello('John').call();
const p2 = routes.utils.sum(5, 2).call();
client.abort();
// both come back as 'request-aborted'
const [, , response1] = await p1;
const [, , response2] = await p2;
if (response1['@thrownErrors']?.[0].type === 'request-aborted')
console.log('first request canceled');
if (response2['@thrownErrors']?.[0].type === 'request-aborted')
console.log('second request canceled');
// new requests work normally after abort
const [greeting] = await routes.sayHello('John').call();
console.log(greeting);
import {createMionRouter, Routes} from '@mionjs/router';
const mion = createMionRouter();
const routes = {
sayHello: mion.route((ctx, name: string): string => `Hello ${name}`),
utils: {
sum: mion.route((ctx, a: number, b: number): number => a + b),
},
} satisfies Routes;
const myApi = mion.initRoutes(routes);
export type MyApi = typeof myApi;
client.destroy() also aborts all in-flight requests before clearing handlers.Cancelling Middleware Calls and Batches
Cancellation and timeouts work the same with middleware and batches.
import {HeadersSubset} from '@mionjs/core';
import {initClient, batch} from '@mionjs/client';
import type {MyApi} from './hello-sum-auth.routes.ts';
const {routes, middlewares} = initClient<MyApi>({
baseURL: 'http://localhost:3000',
});
const controller = new AbortController();
middlewares.trace.onRequest((trace) =>
trace(new HeadersSubset({'X-Trace-Id': crypto.randomUUID()}))
);
// cancellation works with middleware
const [greeting, , response] = await routes.sayHello('John').call({
timeout: 5000,
signal: controller.signal,
});
if (!response['@thrownErrors']) console.log(greeting);
// and with a batch
const [[sum, sumError, batchResponse], [greeting2, greetingError]] =
await batch([routes.utils.sum(5, 2), routes.sayHello('Jane')]).call({
timeout: 10_000,
});
if (batchResponse['@thrownErrors'])
console.log('Batch failed:', batchResponse['@thrownErrors'][0].publicMessage);
else if (!sumError && !greetingError) console.log(sum, greeting2);
import {HeadersSubset, FatalError} from '@mionjs/core';
import {createMionRouter, Routes} from '@mionjs/router';
import {getSession} from './session.ts';
const mion = createMionRouter();
const routes = {
// the client sends a trace id with every request, the server echoes it back
trace: mion.headersMiddleware(
(ctx, {headers}: HeadersSubset<'X-Trace-Id'>): string =>
headers['X-Trace-Id']
),
// runs before every route; a returned FatalError ends the request and stays typed
auth: mion.middleware((ctx): void | FatalError<'not-authorized'> => {
if (!getSession(ctx.request.headers.get('cookie')))
return new FatalError({
publicMessage: 'Not Authorized',
type: 'not-authorized',
});
}),
sayHello: mion.route((ctx, name: string): string => `Hello ${name}`),
utils: {
sum: mion.route((ctx, a: number, b: number): number => a + b),
},
} satisfies Routes;
const myApi = mion.initRoutes(routes);
export type MyApi = typeof myApi;
Aborting Mutations
client.abort(), a signal and a timeout only end the wait on the client. The server sees a dropped connection, and nothing is undone. By then the server may not have received the request, may be halfway through it, or may already have committed it.
Cancelling a query() is always safe, the worst case is wasted work on the server. See Queries and Mutations.