Skip to content
tRPC

Message ports

Serve calls over a MessagePort, a web Worker or a worker_threads port.

handler.messagePort(port) serves calls over a MessagePort, a web Worker or a worker_threads port. Messages are structured-cloned objects in the same format as WebSocket messages, so subscriptions, deferred values, aborts and connectionParams work as they do over wsLink.

// worker.ts
import { const parentPort: MessagePort | nullparentPort } from 'node:worker_threads';

if (!const parentPort: MessagePort | nullparentPort) throw new 
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('worker.ts runs in a worker thread');
createFetchHandler<{
    health: BuildProcedure<RootDef<{}, {}>, "query", {
        output: string;
        errors: never;
        services: never;
        unmapped: never;
    }>;
}, undefined>(opts: FetchHandlerOptions<{
    health: BuildProcedure<RootDef<{}, {}>, "query", {
        output: string;
        errors: never;
        services: never;
        unmapped: never;
    }>;
}, undefined>): FetchHandler<undefined>
createFetchHandler
({
BaseHandlerOptions<{ health: BuildProcedure<RootDef<{}, {}>, "query", { output: string; errors: never; services: never; unmapped: never; }>; }>.router: {
    health: BuildProcedure<RootDef<{}, {}>, "query", {
        output: string;
        errors: never;
        services: never;
        unmapped: never;
    }>;
}
router
,
createContext?: ((opts: CreateContextOpts<undefined>) => MaybePromise<{}>) | undefinedcreateContext: (opts: CreateContextOpts<undefined>opts) => ({ token: unknowntoken: opts: CreateContextOpts<undefined>opts.
CreateContextOpts<undefined>.info: {
    readonly calls: ReadonlyArray<{
        path: string;
        type: ProcedureType;
    }>;
    readonly signal: AbortSignal;
    readonly transport: Transport;
    readonly connectionParams: Record<string, unknown> | undefined;
}
info
.connectionParams: Record<string, unknown> | undefinedconnectionParams?.['token'],
transport: Transporttransport: opts: CreateContextOpts<undefined>opts.
CreateContextOpts<undefined>.info: {
    readonly calls: ReadonlyArray<{
        path: string;
        type: ProcedureType;
    }>;
    readonly signal: AbortSignal;
    readonly transport: Transport;
    readonly connectionParams: Record<string, unknown> | undefined;
}
info
.transport: Transporttransport, // 'messagePort'
}), }).
FetchHandler<undefined>.messagePort(port: MessagePortLike, opts?: {
    request?: Request;
    platform?: undefined;
} | undefined): void
messagePort
(const parentPort: MessagePortparentPort);

On the other side, experimental_messagePortLink sends calls to the port:

import {
  function createTrpcClient<TSource, const TLinks extends ReadonlyArray<TrpcLink<any, RouterOf<TSource>>>>(opts: TypedClientOptions<TSource, TLinks> & DuplicateSwitchCheck<TLinks> & UnmetNeedCheck<TLinks>): TrpcClient<RouterOf<TSource>, LinksContext<TLinks>, undefined, LinksTypes<TLinks>>createTrpcClient,
  
function experimental_messagePortLink<TOpts extends MessagePortLinkOptions>(opts: TOpts): SocketLink<{
    form: FormOfOptions<TOpts>;
}>
experimental_messagePortLink
,
function routerType<TRouter>(): RouterType<TRouter>routerType, } from '@trpc12/trpc/client'; const const worker: Workerworker = new var Worker: new (scriptURL: string | URL, options?: WorkerOptions) => WorkerWorker(new var URL: new (url: string | URL, base?: string | URL) => URLURL('./worker.ts', import.meta.ImportMeta.url: stringurl), { WorkerOptions.type?: WorkerType | undefinedtype: 'module', }); const
const client: TrpcClient<{
    post: {
        byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
            id: ZodString;
        }, $strip>>, "query", {
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }>;
            services: never;
            unmapped: never;
        }>;
        list: BuildProcedure<RootDef<Context, {}>, "query", {
            output: Post[];
            errors: never;
            services: never;
            unmapped: never;
        }>;
        create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
            ctx: Context;
            ctxAdded: {
                ...;
            };
            inputOut: Unset;
            errors: TrpcError<...>;
            shorts: never;
        }>, ZodObject<...>>, "mutation", {
            ...;
        }>;
    };
}, {}, undefined, LinksTypes<...>>
client
=
createTrpcClient<RouterType<{
    post: {
        byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
            id: ZodString;
        }, $strip>>, "query", {
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }>;
            services: never;
            unmapped: never;
        }>;
        list: BuildProcedure<RootDef<Context, {}>, "query", {
            output: Post[];
            errors: never;
            services: never;
            unmapped: never;
        }>;
        create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
            ctx: Context;
            ctxAdded: {
                ...;
            };
            inputOut: Unset;
            errors: TrpcError<...>;
            shorts: never;
        }>, ZodObject<...>>, "mutation", {
            ...;
        }>;
    };
}>, readonly [...]>(opts: TypedClientOptions<...>): TrpcClient<...>
createTrpcClient
({
TypedClientOptions<RouterType<{ post: { byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{ id: ZodString; }, $strip>>, "query", { output: Post; errors: TrpcError<...>; services: never; unmapped: never; }>; list: BuildProcedure<...>; create: BuildProcedure<...>; }; }>, readonly [...]>.router: RouterType<{
    post: {
        byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
            id: ZodString;
        }, $strip>>, "query", {
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }>;
            services: never;
            unmapped: never;
        }>;
        list: BuildProcedure<RootDef<Context, {}>, "query", {
            output: Post[];
            errors: never;
            services: never;
            unmapped: never;
        }>;
        create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
            ctx: Context;
            ctxAdded: {
                ...;
            };
            inputOut: Unset;
            errors: TrpcError<...>;
            shorts: never;
        }>, ZodObject<...>>, "mutation", {
            ...;
        }>;
    };
}>
router
:
routerType<{
    post: {
        byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
            id: ZodString;
        }, $strip>>, "query", {
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }>;
            services: never;
            unmapped: never;
        }>;
        list: BuildProcedure<RootDef<Context, {}>, "query", {
            output: Post[];
            errors: never;
            services: never;
            unmapped: never;
        }>;
        create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
            ctx: Context;
            ctxAdded: {
                ...;
            };
            inputOut: Unset;
            errors: TrpcError<...>;
            shorts: never;
        }>, ZodObject<...>>, "mutation", {
            ...;
        }>;
    };
}>(): RouterType<...>
routerType
<
type AppRouter = {
    post: {
        byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{
            id: ZodString;
        }, $strip>>, "query", {
            output: Post;
            errors: TrpcError<"NOT_FOUND", {
                id: string;
            }>;
            services: never;
            unmapped: never;
        }>;
        list: BuildProcedure<RootDef<Context, {}>, "query", {
            output: Post[];
            errors: never;
            services: never;
            unmapped: never;
        }>;
        create: BuildProcedure<InputDef<With<RootDef<Context, {}>, {
            ctx: Context;
            ctxAdded: {
                ...;
            };
            inputOut: Unset;
            errors: TrpcError<...>;
            shorts: never;
        }>, ZodObject<...>>, "mutation", {
            ...;
        }>;
    };
}
AppRouter
>(),
TypedClientOptions<RouterType<{ post: { byId: BuildProcedure<InputDef<RootDef<Context, {}>, ZodObject<{ id: ZodString; }, $strip>>, "query", { output: Post; errors: TrpcError<...>; services: never; unmapped: never; }>; list: BuildProcedure<...>; create: BuildProcedure<...>; }; }>, readonly [...]>.links: readonly [SocketLink<{
    form: JsonForm;
}>]
links
: [
experimental_messagePortLink<{
    port: Worker;
}>(opts: {
    port: Worker;
}): SocketLink<{
    form: JsonForm;
}>
experimental_messagePortLink
({ port: Workerport: const worker: Workerworker })],
});
  • createContext gets the request passed as handler.messagePort(port, { request }), or a placeholder http://localhost/ request.
  • Bytes in streamed outputs pass as they are, without base64. Files as inputs aren't sent; the call fails with a BAD_REQUEST.
  • Closing the port is up to you. Electron's MessagePortMain and iframe windows work too.