Skip to content

tRPC v12 isn’t on npm yet. The @trpc12/* packages on this site can’t be installed until it’s released. Use v11 today

tRPC

Hidden page. It waits for review: the sidebar, search and search engines leave it out, and listed pages don't link to it.

Publisher

Typed publish/subscribe as an Effect service, with a channel per audience and resumable events.

On this page

Publisher is typed publish/subscribe as an Effect service. Declare a key with its channels, then provide a layer:

import { 
const Publisher: {
    Service: <Self, TEvents extends PublisherEvents>() => <const Key extends string>(key: Key) => PublisherClass<Self, Key, TEvents>;
}
Publisher
} from '@trpc12/trpc/server';
class class PostsPosts extends
const Publisher: {
    Service: <Self, TEvents extends PublisherEvents>() => <const Key extends string>(key: Key) => PublisherClass<Self, Key, TEvents>;
}
Publisher
.
type Service: <Posts, {
    created: Post;
    deleted: {
        id: string;
    };
}>() => <const Key extends string>(key: Key) => PublisherClass<Posts, Key, {
    created: Post;
    deleted: {
        id: string;
    };
}>
Service
<
class PostsPosts, { created: Postcreated: Post;
deleted: {
    id: string;
}
deleted
: { id: stringid: string } }
>()('Posts') {} const
const router: {
    create: Procedure<{
        type: "mutation";
        ctx: {};
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: SchemaValidationError<z.ZodObject<{
            title: z.ZodString;
        }, z.core.$strip>>;
        services: Posts;
    }>;
    onCreated: Procedure<{
        type: "subscription";
        ctx: {};
        meta: {};
        input: undefined;
        output: TrackedEnvelope<Post>;
        errors: never;
        services: Posts;
    }>;
}
router
= {
create: Procedure<{
    type: "mutation";
    ctx: {};
    meta: {};
    input: {
        title: string;
    };
    output: Post;
    errors: SchemaValidationError<z.ZodObject<{
        title: z.ZodString;
    }, z.core.$strip>>;
    services: Posts;
}>
create
: const t: TrpcRoot<{}, {}, RootDef<{}, {}>>t.TrpcRoot<{}, {}, RootDef<{}, {}>>.procedure: ProcedureBuilder<RootDef<{}, {}>>procedure.
ProcedureBuilder<RootDef<{}, {}>>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<InputDef<RootDef<{}, {}>, z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>> (+1 overload)
input
(
const PostInput: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>
PostInput
).
ProcedureBuilder<InputDef<RootDef<{}, {}>, ZodObject<{ title: ZodString; }, $strip>>>.mutation<Effect.Effect<Post, never, Posts>>(resolver: (opts: ResolverOpts<InputDef<RootDef<{}, {}>, z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>>) => Effect.Effect<Post, never, Posts>): Procedure<{
    type: "mutation";
    ctx: {};
    meta: {};
    input: {
        title: string;
    };
    output: Post;
    errors: SchemaValidationError<z.ZodObject<{
        title: z.ZodString;
    }, z.core.$strip>>;
    services: Posts;
}>
mutation
((
opts: ResolverOpts<InputDef<RootDef<{}, {}>, z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>>
opts
) =>
import EffectEffect.const gen: <Effect.Effect<Post, never, never> | Effect.Effect<string, never, Posts>, Post>(f: () => Generator<Effect.Effect<Post, never, never> | Effect.Effect<string, never, Posts>, Post, never>) => Effect.Effect<Post, never, Posts> (+1 overload)gen(function* () { const const post: Postpost = yield*
function savePost(input: {
    title: string;
}): Effect.Effect<Post>
savePost
(
opts: ResolverOpts<InputDef<RootDef<{}, {}>, z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>>
opts
.
ResolverOpts<InputDef<RootDef<{}, {}>, ZodObject<{ title: ZodString; }, $strip>>>.input: {
    title: string;
}
input
);
yield* class PostsPosts.PublisherClass<Posts, "Posts", { created: Post; deleted: { id: string; }; }>.publish<"created">(channel: "created", data: Post): Effect.Effect<string, never, Posts>publish('created', const post: Postpost); return const post: Postpost; }), ),
onCreated: Procedure<{
    type: "subscription";
    ctx: {};
    meta: {};
    input: undefined;
    output: TrackedEnvelope<Post>;
    errors: never;
    services: Posts;
}>
onCreated
: const t: TrpcRoot<{}, {}, RootDef<{}, {}>>t.TrpcRoot<{}, {}, RootDef<{}, {}>>.procedure: ProcedureBuilder<RootDef<{}, {}>>procedure.
ProcedureBuilder<RootDef<{}, {}>>.subscription<Stream<TrackedEnvelope<Post>, never, Posts>>(resolver: (opts: SubscriptionResolverOpts<RootDef<{}, {}>>) => Stream<TrackedEnvelope<Post>, never, Posts>): Procedure<{
    type: "subscription";
    ctx: {};
    meta: {};
    input: undefined;
    output: TrackedEnvelope<Post>;
    errors: never;
    services: Posts;
}>
subscription
((opts: SubscriptionResolverOpts<RootDef<{}, {}>>opts) =>
class PostsPosts.PublisherClass<Posts, "Posts", { created: Post; deleted: { id: string; }; }>.subscribe<"created">(channel: "created", opts?: SubscribeOptions): Stream<TrackedEnvelope<Post>, never, Posts>subscribe('created', { SubscribeOptions.lastEventId?: string | undefinedlastEventId: opts: SubscriptionResolverOpts<RootDef<{}, {}>>opts.SubscriptionResolverOpts<RootDef<{}, {}>>.lastEventId: string | undefinedlastEventId }), ), };
createFetchHandler<{
    create: Procedure<{
        type: "mutation";
        ctx: {};
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: SchemaValidationError<z.ZodObject<{
            title: z.ZodString;
        }, z.core.$strip>>;
        services: Posts;
    }>;
    onCreated: Procedure<{
        type: "subscription";
        ctx: {};
        meta: {};
        input: undefined;
        output: TrackedEnvelope<Post>;
        errors: never;
        services: Posts;
    }>;
}, undefined>(opts: FetchHandlerOptions<{
    create: Procedure<{
        type: "mutation";
        ctx: {};
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: SchemaValidationError<z.ZodObject<{
            title: z.ZodString;
        }, z.core.$strip>>;
        services: Posts;
    }>;
    onCreated: Procedure<{
        type: "subscription";
        ctx: {};
        meta: {};
        input: undefined;
        output: TrackedEnvelope<Post>;
        errors: never;
        services: Posts;
    }>;
}, undefined>): FetchHandler<...>
createFetchHandler
({
BaseHandlerOptions<{ create: Procedure<{ type: "mutation"; ctx: {}; meta: {}; input: { title: string; }; output: Post; errors: SchemaValidationError<ZodObject<{ title: ZodString; }, $strip>>; services: Posts; }>; onCreated: Procedure<...>; }>.router: {
    create: Procedure<{
        type: "mutation";
        ctx: {};
        meta: {};
        input: {
            title: string;
        };
        output: Post;
        errors: SchemaValidationError<z.ZodObject<{
            title: z.ZodString;
        }, z.core.$strip>>;
        services: Posts;
    }>;
    onCreated: Procedure<{
        type: "subscription";
        ctx: {};
        meta: {};
        input: undefined;
        output: TrackedEnvelope<Post>;
        errors: never;
        services: Posts;
    }>;
}
router
, layer: Layer<Posts, any, never>layer: class PostsPosts.PublisherClass<Posts, "Posts", { created: Post; deleted: { id: string; }; }>.memory(opts?: MemoryPublisherOptions): Layer<Posts, never, never>memory({ MemoryPublisherOptions.buffer?: number | undefinedbuffer: 100 }) });

subscribe() yields tracked() events with the publisher's ids, so reconnects resume. Posts.memory({ buffer }) serves one process on a PubSub and keeps buffer events per channel for resumes. For several processes, provide another layer with the same shape (publish, subscribe), built on the clients Effect already ships (Backends).

A channel per audience

To reach some subscribers only (a room, a user's inbox), give each audience its own channel. A template literal key types every one of them; take the subscriber's channel from ctx on the server, never from client input:

class class NotesNotes extends 
const Publisher: {
    Service: <Self, TEvents extends PublisherEvents>() => <const Key extends string>(key: Key) => PublisherClass<Self, Key, TEvents>;
}
Publisher
.
type Service: <Notes, {
    [inbox: `inbox:${string}`]: {
        text: string;
    };
}>() => <const Key extends string>(key: Key) => PublisherClass<Notes, Key, {
    [inbox: `inbox:${string}`]: {
        text: string;
    };
}>
Service
<
class NotesNotes, { [inbox: `inbox:${string}`inbox: `inbox:${string}`]: { text: stringtext: string } } >()('Notes') {} const
const router: {
    notify: Procedure<{
        type: "mutation";
        ctx: {
            user: User;
        };
        meta: {};
        input: {
            readonly to: string;
            readonly text: string;
        };
        output: string;
        errors: SchemaValidationError<Schema.Struct<{
            readonly to: Schema.String;
            readonly text: Schema.String;
        }>>;
        services: Notes;
    }>;
    inbox: Procedure<{
        type: "subscription";
        ctx: {
            user: User;
        };
        meta: {};
        input: undefined;
        output: TrackedEnvelope<{
            text: string;
        }>;
        errors: never;
        services: Notes;
    }>;
}
router
= {
notify: Procedure<{
    type: "mutation";
    ctx: {
        user: User;
    };
    meta: {};
    input: {
        readonly to: string;
        readonly text: string;
    };
    output: string;
    errors: SchemaValidationError<Schema.Struct<{
        readonly to: Schema.String;
        readonly text: Schema.String;
    }>>;
    services: Notes;
}>
notify
:
const t: TrpcRoot<{
    user: User;
}, {}, RootDef<{
    user: User;
}, {}>>
t
.
TrpcRoot<{ user: User; }, {}, RootDef<{ user: User; }, {}>>.procedure: ProcedureBuilder<RootDef<{
    user: User;
}, {}>>
procedure
.
ProcedureBuilder<RootDef<{ user: User; }, {}>>.input<Schema.Struct<{
    readonly to: Schema.String;
    readonly text: Schema.String;
}>>(schema: Schema.Struct<{
    readonly to: Schema.String;
    readonly text: Schema.String;
}>): ProcedureBuilder<InputDef<RootDef<{
    user: User;
}, {}>, Schema.Struct<{
    readonly to: Schema.String;
    readonly text: Schema.String;
}>>> (+1 overload)
input
(import SchemaSchema.
function Struct<{
    readonly to: Schema.String;
    readonly text: Schema.String;
}>(fields: {
    readonly to: Schema.String;
    readonly text: Schema.String;
}): Schema.Struct<{
    readonly to: Schema.String;
    readonly text: Schema.String;
}>
Struct
({ to: Schema.Stringto: import SchemaSchema.const String: Schema.StringString, text: Schema.Stringtext: import SchemaSchema.const String: Schema.StringString }))
.
ProcedureBuilder<InputDef<RootDef<{ user: User; }, {}>, Struct<{ readonly to: String; readonly text: String; }>>>.mutation<Effect<string, never, Notes>>(resolver: (opts: ResolverOpts<InputDef<RootDef<{
    user: User;
}, {}>, Schema.Struct<{
    readonly to: Schema.String;
    readonly text: Schema.String;
}>>>) => Effect<string, never, Notes>): Procedure<{
    type: "mutation";
    ctx: {
        user: User;
    };
    meta: {};
    input: {
        readonly to: string;
        readonly text: string;
    };
    output: string;
    errors: SchemaValidationError<Schema.Struct<{
        readonly to: Schema.String;
        readonly text: Schema.String;
    }>>;
    services: Notes;
}>
mutation
((
opts: ResolverOpts<InputDef<RootDef<{
    user: User;
}, {}>, Schema.Struct<{
    readonly to: Schema.String;
    readonly text: Schema.String;
}>>>
opts
) =>
class NotesNotes.
PublisherClass<Notes, "Notes", { [inbox: `inbox:${string}`]: { text: string; }; }>.publish<`inbox:${string}`>(channel: `inbox:${string}`, data: {
    text: string;
}): Effect<string, never, Notes>
publish
(`inbox:${
opts: ResolverOpts<InputDef<RootDef<{
    user: User;
}, {}>, Schema.Struct<{
    readonly to: Schema.String;
    readonly text: Schema.String;
}>>>
opts
.
ResolverOpts<InputDef<RootDef<{ user: User; }, {}>, Struct<{ readonly to: String; readonly text: String; }>>>.input: {
    readonly to: string;
    readonly text: string;
}
input
.to: stringto}`, { text: stringtext:
opts: ResolverOpts<InputDef<RootDef<{
    user: User;
}, {}>, Schema.Struct<{
    readonly to: Schema.String;
    readonly text: Schema.String;
}>>>
opts
.
ResolverOpts<InputDef<RootDef<{ user: User; }, {}>, Struct<{ readonly to: String; readonly text: String; }>>>.input: {
    readonly to: string;
    readonly text: string;
}
input
.text: stringtext }),
),
inbox: Procedure<{
    type: "subscription";
    ctx: {
        user: User;
    };
    meta: {};
    input: undefined;
    output: TrackedEnvelope<{
        text: string;
    }>;
    errors: never;
    services: Notes;
}>
inbox
:
const t: TrpcRoot<{
    user: User;
}, {}, RootDef<{
    user: User;
}, {}>>
t
.
TrpcRoot<{ user: User; }, {}, RootDef<{ user: User; }, {}>>.procedure: ProcedureBuilder<RootDef<{
    user: User;
}, {}>>
procedure
.
ProcedureBuilder<RootDef<{ user: User; }, {}>>.subscription<Stream<TrackedEnvelope<{
    text: string;
}>, never, Notes>>(resolver: (opts: SubscriptionResolverOpts<RootDef<{
    user: User;
}, {}>>) => Stream<TrackedEnvelope<{
    text: string;
}>, never, Notes>): Procedure<{
    type: "subscription";
    ctx: {
        user: User;
    };
    meta: {};
    input: undefined;
    output: TrackedEnvelope<{
        text: string;
    }>;
    errors: never;
    services: Notes;
}>
subscription
((
opts: SubscriptionResolverOpts<RootDef<{
    user: User;
}, {}>>
opts
) =>
class NotesNotes.
PublisherClass<Notes, "Notes", { [inbox: `inbox:${string}`]: { text: string; }; }>.subscribe<`inbox:${string}`>(channel: `inbox:${string}`, opts?: SubscribeOptions): Stream<TrackedEnvelope<{
    text: string;
}>, never, Notes>
subscribe
(`inbox:${
opts: SubscriptionResolverOpts<RootDef<{
    user: User;
}, {}>>
opts
.
ResolverOpts<RootDef<{ user: User; }, {}>>.ctx: {
    user: User;
}
ctx
.user: Useruser.User.id: stringid}`, {
SubscribeOptions.lastEventId?: string | undefinedlastEventId:
opts: SubscriptionResolverOpts<RootDef<{
    user: User;
}, {}>>
opts
.SubscriptionResolverOpts<RootDef<{ user: User; }, {}>>.lastEventId: string | undefinedlastEventId,
}), ), };

The backend filters by channel, so a process only receives its subscribers' channels (Redis subscribes per channel). To reach several audiences, publish to each channel.

Credits

The Publisher follows oRPC's publisher helpers.