The screenshot API for developers -
Try ScreenshotOne
Skip to content

Hibernation Integration

Hibernation integration lets oRPC leverage Hibernation APIs like Cloudflare's Hibernation WebSocket, so your server can sleep between events without dropping active connections.

Installation

sh
npm install @orpc/hibernation@beta
sh
yarn add @orpc/hibernation@beta
sh
pnpm add @orpc/hibernation@beta
sh
bun add @orpc/hibernation@beta
sh
deno add npm:@orpc/hibernation@beta

Setup

ts
import { HibernationHandlerPlugin } from '@orpc/hibernation'

const handler = new RPCHandler(router, {
  plugins: [
    new HibernationHandlerPlugin(),
  ],
})

WARNING

When combined with the Batch Plugin, 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.

Usage

The plugin provides HibernationAsyncIteratorClass and encodeHibernationRPCEvent to help you return an Async Iterator Object that utilizes the Hibernation APIs.

  1. Return a HibernationAsyncIteratorClass from your handler

    ts
    import { HibernationAsyncIteratorClass } from '@orpc/hibernation'
    
    const base = os.$context<{ ws: WebSocket }>()
    
    export const onMessage = base.handler(async ({ context }) => {
      return new HibernationAsyncIteratorClass<{ message: string }>((id) => {
        // Save the ID. You'll need it to send events later.
        context.ws.serializeAttachment({ id })
      })
    })
  2. Send events to clients with encodeHibernationRPCEvent

    ts
    import { encodeHibernationRPCEvent } from '@orpc/hibernation'
    import * as z from 'zod'
    
    const base = os.$context<{ getWebSockets: () => WebSocket[] }>()
    
    export const sendMessage = base
      .input(z.object({ message: z.string() }))
      .handler(async ({ input, context }) => {
        const websockets = context.getWebSockets()
    
        for (const ws of websockets) {
          const { id } = ws.deserializeAttachment()
    
          // yield an event to all clients
          ws.send(await encodeHibernationRPCEvent(id, { message: input.message }, {
            // override the default RPC serializer if needed
            serializer: new RPCSerializer(),
          }))
          // return an event and stop the iterator
          ws.send(await encodeHibernationRPCEvent(id, { message: input.message }, { event: 'close' }))
          // throw an error and stop the iterator
          ws.send(await encodeHibernationRPCEvent(id, new ORPCError('INTERNAL_SERVER_ERROR'), { event: 'error' }))
        }
      })
Cloudflare Durable Object Chat Room Example?

This example shows how to build a chat room with Cloudflare Durable Objects and WebSocket Hibernation. Everyone connected to the same Durable Object can exchange messages. You can try a working version in the Cloudflare Playground, see Playgrounds.

ts
import { RPCHandler } from '@orpc/server/websocket'
import {
  encodeHibernationRPCEvent,
  HibernationAsyncIteratorClass,
  HibernationHandlerPlugin,
} from '@orpc/hibernation'
import { onError, os } from '@orpc/server'
import { DurableObject } from 'cloudflare:workers'
import * as z from 'zod'

const base = os.$context<{
  handler: RPCHandler<any>
  ws: WebSocket
  getWebsockets: () => WebSocket[]
}>()

export const router = {
  send: base.input(z.object({ message: z.string() })).handler(async ({ input, context }) => {
    const websockets = context.getWebsockets()

    for (const ws of websockets) {
      const data = ws.deserializeAttachment()
      if (typeof data !== 'object' || data === null) {
        continue
      }

      const { id } = data

      ws.send(await encodeHibernationRPCEvent(id, input.message))
    }
  }),
  onMessage: base.handler(async ({ context }) => {
    return new HibernationAsyncIteratorClass<string>((id) => {
      context.ws.serializeAttachment({ id })
    })
  }),
}

const handler = new RPCHandler(router, {
  interceptors: [
    onError((error) => {
      console.error(error)
    }),
  ],
  plugins: [
    new HibernationHandlerPlugin(),
  ],
})

export class ChatRoom extends DurableObject {
  async fetch(): Promise<Response> {
    const { '0': client, '1': server } = new WebSocketPair()

    this.ctx.acceptWebSocket(server)

    return new Response(null, {
      status: 101,
      webSocket: client,
    })
  }

  async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
    await handler.message(ws, message, {
      context: {
        handler,
        ws,
        getWebsockets: () => this.ctx.getWebSockets(),
      },
    })
  }

  async webSocketClose(ws: WebSocket): Promise<void> {
    await handler.close(ws)
  }
}
ts
import { RPCLink } from '@orpc/client/websocket'
import { createORPCClient } from '@orpc/client'
import type { router } from '../../worker/dos/chat-room'
import type { RouterClient } from '@orpc/server'

const websocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/chat-room`)

websocket.addEventListener('error', (event) => {
  console.error(event)
})

const link = new RPCLink({
  connect: () => websocket,
})

export const chatRoomClient: RouterClient<typeof router> = createORPCClient(link)
tsx
import { useEffect, useState } from 'react'
import { chatRoomClient } from '../lib/chat-room'

export function ChatRoom() {
  const [messages, setMessages] = useState<string[]>([])

  useEffect(() => {
    const controller = new AbortController()

    void (async () => {
      for await (const message of await chatRoomClient.onMessage(undefined, { signal: controller.signal })) {
        setMessages(messages => [...messages, message])
      }
    })()

    return () => {
      controller.abort()
    }
  }, [])

  const sendMessage = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()

    const form = new FormData(e.target as HTMLFormElement)
    const message = form.get('message') as string

    await chatRoomClient.send({ message })
  }

  return (
    <div>
      <h1>Chat Room</h1>
      <p>Open multiple tabs to chat together</p>
      <ul>
        {messages.map((message, index) => (
          <li key={index}>{message}</li>
        ))}
      </ul>
      <form onSubmit={sendMessage}>
        <input name="message" type="text" required defaultValue="hello" />
        <button type="submit">Send</button>
      </form>
    </div>
  )
}

Released under the MIT License.