Skip to content

Commit dd4e882

Browse files
authored
feat(server): event publisher (#603)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new event publishing and subscription system, enabling real-time event broadcasting and consumption with support for both callback listeners and async iterators. - Added options for event buffering and cancellation when consuming events asynchronously. - **Documentation** - Extended documentation with a new section detailing usage patterns and code examples for the event publishing system. - **Tests** - Added comprehensive tests to verify type safety and runtime behavior of the new event publishing system. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent d177d36 commit dd4e882

File tree

10 files changed

+558
-11
lines changed

10 files changed

+558
-11
lines changed

apps/content/docs/event-iterator.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,54 @@ const example = os
105105
}
106106
})
107107
```
108+
109+
## Event Publisher
110+
111+
oRPC includes a built-in `EventPublisher` for real-time features like chat, notifications, or live updates. It supports broadcasting and subscribing to named events.
112+
113+
::: code-group
114+
115+
```ts [Static Events]
116+
import { EventPublisher } from '@orpc/server'
117+
118+
const publisher = new EventPublisher<{
119+
'something-updated': {
120+
id: string
121+
}
122+
}>()
123+
124+
const livePlanet = os
125+
.handler(async function* ({ input, signal }) {
126+
for await (const payload of publisher.subscribe('something-updated', { signal })) { // [!code highlight]
127+
// handle payload here and yield something to client
128+
}
129+
})
130+
131+
const update = os
132+
.input(z.object({ id: z.string() }))
133+
.handler(async function* ({ input }) {
134+
publisher.publish('something-updated', { id: input.id }) // [!code highlight]
135+
})
136+
```
137+
138+
```ts [Dynamic Events]
139+
import { EventPublisher } from '@orpc/server'
140+
141+
const publisher = new EventPublisher<Record<string, { message: string }>>()
142+
143+
const onMessage = os
144+
.input(z.object({ channel: z.string() }))
145+
.handler(async function* ({ input, signal }) {
146+
for await (const payload of publisher.subscribe(input.channel, { signal })) { // [!code highlight]
147+
yield payload.message
148+
}
149+
})
150+
151+
const sendMessage = os
152+
.input(z.object({ channel: z.string(), message: z.string() }))
153+
.handler(async function* ({ input }) {
154+
publisher.publish(input.channel, { message: input.message }) // [!code highlight]
155+
})
156+
```
157+
158+
:::

packages/server/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@ export type {
3939
Schema,
4040
} from '@orpc/contract'
4141
export type { IntersectPick } from '@orpc/shared'
42-
export { onError, onFinish, onStart, onSuccess } from '@orpc/shared'
43-
export type { Registry, ThrowableError } from '@orpc/shared'
42+
export { EventPublisher, onError, onFinish, onStart, onSuccess } from '@orpc/shared'
43+
export type { EventPublisherOptions, EventPublisherSubscribeIteratorOptions, Registry, ThrowableError } from '@orpc/shared'
4444
export { getEventMeta, withEventMeta } from '@orpc/standard-server'
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { EventPublisher } from './event-publisher'
2+
3+
describe('eventPublisher', () => {
4+
it('key-value types', async () => {
5+
const pub = new EventPublisher<{
6+
'event-1': {
7+
id: string
8+
}
9+
'event-2': {
10+
name: string
11+
}
12+
}>()
13+
14+
pub.publish('event-1', { id: '1' })
15+
pub.publish('event-2', { name: '1' })
16+
// @ts-expect-error - wrong event
17+
pub.publish('event-3', { name: '1' })
18+
// @ts-expect-error - wrong payload
19+
pub.publish('event-2', { name: 123 })
20+
21+
pub.subscribe('event-1', (payload) => {
22+
expectTypeOf(payload).toEqualTypeOf<{
23+
id: string
24+
}>()
25+
})
26+
27+
pub.subscribe('event-2', (payload) => {
28+
expectTypeOf(payload).toEqualTypeOf<{
29+
name: string
30+
}>()
31+
})
32+
33+
// @ts-expect-error - wrong event
34+
pub.subscribe('event-3', (payload) => {})
35+
36+
for await (const payload of pub.subscribe('event-1')) {
37+
expectTypeOf(payload).toEqualTypeOf<{
38+
id: string
39+
}>()
40+
}
41+
42+
for await (const payload of pub.subscribe('event-2')) {
43+
expectTypeOf(payload).toEqualTypeOf<{
44+
name: string
45+
}>()
46+
}
47+
48+
// @ts-expect-error - wrong event
49+
for await (const payload of pub.subscribe('event-3')) {
50+
// empty
51+
}
52+
})
53+
54+
it('record types', async () => {
55+
const pub = new EventPublisher<Record<string, { id: string }>>()
56+
57+
pub.publish('event-1', { id: '1' })
58+
pub.publish('event-2', { id: '1' })
59+
pub.publish('event-100', { id: '1' })
60+
// @ts-expect-error - wrong event
61+
pub.publish('event-100', { id: 123 })
62+
63+
pub.subscribe('event-933', (payload) => {
64+
expectTypeOf(payload).toEqualTypeOf<{
65+
id: string
66+
}>()
67+
})
68+
69+
for await (const payload of pub.subscribe('event-3439')) {
70+
expectTypeOf(payload).toEqualTypeOf<{
71+
id: string
72+
}>()
73+
}
74+
})
75+
})

0 commit comments

Comments
 (0)