Skip to content

Commit db548c8

Browse files
authored
feat(hibernation): Hibernation APIs (#1763)
Ports the v1 `@orpc/server/hibernation` module to v2 as a dedicated `@orpc/hibernation` package, so Cloudflare WebSocket Hibernation (and similar) setups work again on v2. The plugin preserves `HibernationAsyncIteratorClass` outputs through the RPC codec, and `encodeHibernationRPCEvent` emits peer `event-stream` messages that are byte-identical to what the normal transmitter sends — clients consume hibernated streams exactly like regular ones. ## API changes vs v1 - v2 naming (`HibernationHandlerPlugin`, `HibernationAsyncIteratorClass`) with deprecated v1 aliases so old imports keep compiling and point at the new names. - `encodeHibernationRPCEvent` is now async, takes a `serializer` instance (shareable with `RPCHandler`) and a dedicated `encodePeerMessage` options field; the `'done'` event became `'close'` to match the peer protocol. - The plugin runs as a routing interceptor and declares `before: ['~batch']`; evlog/pino logging plugins now order themselves before `~hibernation` as well. ## Docs - New Plugins page with v2 imports and an updated Cloudflare Durable Object chat-room example, including a warning that hibernation procedures must be excluded from batching. - `@orpc/hibernation` added to the package list in all READMEs. ## Testing - 14 new tests cover the plugin (standard handler + websocket end-to-end: the callback receives the request id, only the response message hits the wire, later events decode as `event-stream` messages for that id) and the encoder (byte parity with `encodePeerMessage`, event meta, `close`/`error` events, prefix, undefined payloads). - Full `pnpm type:check`, lint, and `unbuild` pass; server/evlog/pino suites unaffected (450 tests green).
1 parent d9d031f commit db548c8

47 files changed

Lines changed: 1179 additions & 4 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ You can read the documentation [here](https://orpc.dev).
4444

4545
- [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher): Pub/Sub with memory, Redis, and Upstash adapters.
4646
- [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit): Rate limiting with memory, Redis, and Upstash adapters.
47+
- [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation): Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api).
4748
- [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema): Smart coercion for OpenAPI requests.
4849

4950
**Framework & ecosystem integrations**

apps/content/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export default withMermaid(defineConfig({
191191
{ text: 'ArkType', link: '/docs/integrations/arktype' },
192192
{ text: 'Effect', link: '/docs/integrations/effect' },
193193
{ text: 'Evlog', link: '/docs/integrations/evlog' },
194+
{ text: 'Hibernation', link: '/docs/integrations/hibernation' },
194195
{ text: 'NestJS', link: '/docs/integrations/nest' },
195196
{ text: 'Next.js', link: '/docs/integrations/next' },
196197
{ text: 'OpenTelemetry', link: '/docs/integrations/opentelemetry' },
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
# Hibernation Integration
2+
3+
Hibernation integration lets oRPC leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api), so your server can sleep between events without dropping active connections.
4+
5+
## Installation
6+
7+
::: code-group
8+
9+
```sh [npm]
10+
npm install @orpc/hibernation@beta
11+
```
12+
13+
```sh [yarn]
14+
yarn add @orpc/hibernation@beta
15+
```
16+
17+
```sh [pnpm]
18+
pnpm add @orpc/hibernation@beta
19+
```
20+
21+
```sh [bun]
22+
bun add @orpc/hibernation@beta
23+
```
24+
25+
```sh [deno]
26+
deno add npm:@orpc/hibernation@beta
27+
```
28+
29+
:::
30+
31+
## Setup
32+
33+
```ts
34+
import { HibernationHandlerPlugin } from '@orpc/hibernation'
35+
36+
const handler = new RPCHandler(router, {
37+
plugins: [
38+
new HibernationHandlerPlugin(),
39+
],
40+
})
41+
```
42+
43+
::: warning
44+
When combined with the [Batch Plugin](/docs/plugins/batch), make sure procedures that return a `HibernationAsyncIteratorClass` are excluded from batching (e.g. via the batch link plugin's `filter` option), because hibernation cannot work through batched responses.
45+
:::
46+
47+
## Usage
48+
49+
The plugin provides `HibernationAsyncIteratorClass` and `encodeHibernationRPCEvent` to help you return an [Async Iterator Object](/docs/async-iterator-object) that utilizes the Hibernation APIs.
50+
51+
1. Return a `HibernationAsyncIteratorClass` from your handler
52+
53+
```ts
54+
import { HibernationAsyncIteratorClass } from '@orpc/hibernation'
55+
56+
const base = os.$context<{ ws: WebSocket }>()
57+
58+
export const onMessage = base.handler(async ({ context }) => {
59+
return new HibernationAsyncIteratorClass<{ message: string }>((id) => {
60+
// Save the ID. You'll need it to send events later.
61+
context.ws.serializeAttachment({ id })
62+
})
63+
})
64+
```
65+
66+
2. Send events to clients with `encodeHibernationRPCEvent`
67+
68+
```ts
69+
import { encodeHibernationRPCEvent } from '@orpc/hibernation'
70+
import * as z from 'zod'
71+
72+
const base = os.$context<{ getWebSockets: () => WebSocket[] }>()
73+
74+
export const sendMessage = base
75+
.input(z.object({ message: z.string() }))
76+
.handler(async ({ input, context }) => {
77+
const websockets = context.getWebSockets()
78+
79+
for (const ws of websockets) {
80+
const { id } = ws.deserializeAttachment()
81+
82+
// yield an event to all clients
83+
ws.send(await encodeHibernationRPCEvent(id, { message: input.message }, {
84+
// override the default RPC serializer if needed
85+
serializer: new RPCSerializer(),
86+
}))
87+
// return an event and stop the iterator
88+
ws.send(await encodeHibernationRPCEvent(id, { message: input.message }, { event: 'close' }))
89+
// throw an error and stop the iterator
90+
ws.send(await encodeHibernationRPCEvent(id, new ORPCError('INTERNAL_SERVER_ERROR'), { event: 'error' }))
91+
}
92+
})
93+
```
94+
95+
::: details Cloudflare Durable Object Chat Room Example?
96+
97+
This example shows how to build a chat room with [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/) and [WebSocket Hibernation](https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation-server/). Everyone connected to the same Durable Object can exchange messages. You can try a working version in the Cloudflare Playground, see [Playgrounds](/docs/playgrounds).
98+
99+
::: code-group
100+
101+
```ts [Durable Object]
102+
import { RPCHandler } from '@orpc/server/websocket'
103+
import {
104+
encodeHibernationRPCEvent,
105+
HibernationAsyncIteratorClass,
106+
HibernationHandlerPlugin,
107+
} from '@orpc/hibernation'
108+
import { onError, os } from '@orpc/server'
109+
import { DurableObject } from 'cloudflare:workers'
110+
import * as z from 'zod'
111+
112+
const base = os.$context<{
113+
handler: RPCHandler<any>
114+
ws: WebSocket
115+
getWebsockets: () => WebSocket[]
116+
}>()
117+
118+
export const router = {
119+
send: base.input(z.object({ message: z.string() })).handler(async ({ input, context }) => {
120+
const websockets = context.getWebsockets()
121+
122+
for (const ws of websockets) {
123+
const data = ws.deserializeAttachment()
124+
if (typeof data !== 'object' || data === null) {
125+
continue
126+
}
127+
128+
const { id } = data
129+
130+
ws.send(await encodeHibernationRPCEvent(id, input.message))
131+
}
132+
}),
133+
onMessage: base.handler(async ({ context }) => {
134+
return new HibernationAsyncIteratorClass<string>((id) => {
135+
context.ws.serializeAttachment({ id })
136+
})
137+
}),
138+
}
139+
140+
const handler = new RPCHandler(router, {
141+
interceptors: [
142+
onError((error) => {
143+
console.error(error)
144+
}),
145+
],
146+
plugins: [
147+
new HibernationHandlerPlugin(),
148+
],
149+
})
150+
151+
export class ChatRoom extends DurableObject {
152+
async fetch(): Promise<Response> {
153+
const { '0': client, '1': server } = new WebSocketPair()
154+
155+
this.ctx.acceptWebSocket(server)
156+
157+
return new Response(null, {
158+
status: 101,
159+
webSocket: client,
160+
})
161+
}
162+
163+
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
164+
await handler.message(ws, message, {
165+
context: {
166+
handler,
167+
ws,
168+
getWebsockets: () => this.ctx.getWebSockets(),
169+
},
170+
})
171+
}
172+
173+
async webSocketClose(ws: WebSocket): Promise<void> {
174+
await handler.close(ws)
175+
}
176+
}
177+
```
178+
179+
```ts [Client]
180+
import { RPCLink } from '@orpc/client/websocket'
181+
import { createORPCClient } from '@orpc/client'
182+
import type { router } from '../../worker/dos/chat-room'
183+
import type { RouterClient } from '@orpc/server'
184+
185+
const websocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/chat-room`)
186+
187+
websocket.addEventListener('error', (event) => {
188+
console.error(event)
189+
})
190+
191+
const link = new RPCLink({
192+
connect: () => websocket,
193+
})
194+
195+
export const chatRoomClient: RouterClient<typeof router> = createORPCClient(link)
196+
```
197+
198+
```tsx [Component]
199+
import { useEffect, useState } from 'react'
200+
import { chatRoomClient } from '../lib/chat-room'
201+
202+
export function ChatRoom() {
203+
const [messages, setMessages] = useState<string[]>([])
204+
205+
useEffect(() => {
206+
const controller = new AbortController()
207+
208+
void (async () => {
209+
for await (const message of await chatRoomClient.onMessage(undefined, { signal: controller.signal })) {
210+
setMessages(messages => [...messages, message])
211+
}
212+
})()
213+
214+
return () => {
215+
controller.abort()
216+
}
217+
}, [])
218+
219+
const sendMessage = async (e: React.FormEvent<HTMLFormElement>) => {
220+
e.preventDefault()
221+
222+
const form = new FormData(e.target as HTMLFormElement)
223+
const message = form.get('message') as string
224+
225+
await chatRoomClient.send({ message })
226+
}
227+
228+
return (
229+
<div>
230+
<h1>Chat Room</h1>
231+
<p>Open multiple tabs to chat together</p>
232+
<ul>
233+
{messages.map((message, index) => (
234+
<li key={index}>{message}</li>
235+
))}
236+
</ul>
237+
<form onSubmit={sendMessage}>
238+
<input name="message" type="text" required defaultValue="hello" />
239+
<button type="submit">Send</button>
240+
</form>
241+
</div>
242+
)
243+
}
244+
```
245+
246+
:::

packages/ai-sdk/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ You can read the documentation [here](https://orpc.dev).
4444

4545
- [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher): Pub/Sub with memory, Redis, and Upstash adapters.
4646
- [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit): Rate limiting with memory, Redis, and Upstash adapters.
47+
- [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation): Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api).
4748
- [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema): Smart coercion for OpenAPI requests.
4849

4950
**Framework & ecosystem integrations**

packages/arktype/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ You can read the documentation [here](https://orpc.dev).
4444

4545
- [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher): Pub/Sub with memory, Redis, and Upstash adapters.
4646
- [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit): Rate limiting with memory, Redis, and Upstash adapters.
47+
- [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation): Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api).
4748
- [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema): Smart coercion for OpenAPI requests.
4849

4950
**Framework & ecosystem integrations**

packages/bun/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ You can read the documentation [here](https://orpc.dev).
4141

4242
- [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher): Pub/Sub with memory, Redis, and Upstash adapters.
4343
- [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit): Rate limiting with memory, Redis, and Upstash adapters.
44+
- [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation): Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api).
4445
- [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema): Smart coercion for OpenAPI requests.
4546

4647
**Framework & ecosystem integrations**

packages/client/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ You can read the documentation [here](https://orpc.dev).
4444

4545
- [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher): Pub/Sub with memory, Redis, and Upstash adapters.
4646
- [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit): Rate limiting with memory, Redis, and Upstash adapters.
47+
- [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation): Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api).
4748
- [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema): Smart coercion for OpenAPI requests.
4849

4950
**Framework & ecosystem integrations**

packages/cloudflare/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ You can read the documentation [here](https://orpc.dev).
4444

4545
- [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher): Pub/Sub with memory, Redis, and Upstash adapters.
4646
- [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit): Rate limiting with memory, Redis, and Upstash adapters.
47+
- [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation): Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api).
4748
- [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema): Smart coercion for OpenAPI requests.
4849

4950
**Framework & ecosystem integrations**

packages/contract/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ You can read the documentation [here](https://orpc.dev).
4444

4545
- [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher): Pub/Sub with memory, Redis, and Upstash adapters.
4646
- [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit): Rate limiting with memory, Redis, and Upstash adapters.
47+
- [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation): Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api).
4748
- [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema): Smart coercion for OpenAPI requests.
4849

4950
**Framework & ecosystem integrations**

packages/effect/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ You can read the documentation [here](https://orpc.dev).
4444

4545
- [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher): Pub/Sub with memory, Redis, and Upstash adapters.
4646
- [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit): Rate limiting with memory, Redis, and Upstash adapters.
47+
- [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation): Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api).
4748
- [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema): Smart coercion for OpenAPI requests.
4849

4950
**Framework & ecosystem integrations**

0 commit comments

Comments
 (0)