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.