Skip to content

Subscriptions

tRPC v11 uses async iterables for subscriptions. This library natively supports them over MessagePort, giving you real-time data streaming between Electron processes.

Use t.procedure.subscription() to define a subscription that yields values over time:

src/main/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
onTimer: t.procedure
.input(z.object({ intervalMs: z.number().min(100) }))
.subscription(async function* ({ input, signal }) {
let count = 0;
while (!signal.aborted) {
yield { count: count++, timestamp: new Date() };
await new Promise((resolve) => setTimeout(resolve, input.intervalMs));
}
}),
});
src/renderer/index.ts
import { createTRPCClient } from '@trpc/client';
import { portLink, getPort } from 'electron-messageport-trpc/renderer';
import type { AppRouter } from '../main/router';
const trpc = createTRPCClient<AppRouter>({
links: [portLink({ port: getPort() })],
});
// Subscribe and iterate over values
const subscription = trpc.onTimer.subscribe(
{ intervalMs: 1000 },
{
onStarted() {
console.log('Subscription started');
},
onData(value) {
// value is typed as { count: number; timestamp: Date }
console.log(`Tick #${value.count} at ${value.timestamp}`);
},
onError(err) {
console.error('Subscription error:', err);
},
onStopped() {
console.log('Subscription stopped by the server');
},
onComplete() {
console.log('Subscription ended');
},
},
);
// Later, unsubscribe to stop the stream
subscription.unsubscribe();
  1. The client sends a { kind: 'request', method: 'subscription', ... } message.
  2. The server calls the procedure and receives an async iterable.
  3. The server sends { kind: 'result', type: 'started' }.
  4. For each yielded value, the server sends { kind: 'result', type: 'data', data }.
  5. When the client calls unsubscribe(), it sends { kind: 'subscription.stop', id }.
  6. The server aborts the AbortController, which signals the async generator to stop.
  7. If the iterable completes naturally, the server sends { kind: 'result', type: 'stopped' }.

If the subscription procedure throws an error, the server sends a { kind: 'error' } message with the tRPC error shape. The client’s onError callback receives a TRPCClientError instance.

  • Client unsubscribes: sends subscription.stop, server aborts the AbortController
  • Port closes: the server’s close event handler aborts all active subscriptions
  • destroy() called: same as port close — all subscriptions are aborted and the port is closed