GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • oRPC
  • Documentation
  • Blog
  • Comparison
  • From V1
  • Sponsors
  • Experts
  • Releases
  • Discussions
  • LLM Context
  • LLM Context (Full)
  • V1 Documentation
  • Contract-First
  • Requirements

This view

  • Original page
  • New address

GeezerVison is showing orpc.dev — AsyncIteratorObject (SSE) - oRPC

On this page
  • Overview
  • Validating Events
  • Last Event ID & Event Metadata
  • Stop AsyncIteratorObject
  • Signal and Side-Effects
  • Publisher Helper

AsyncIteratorObject (SSE)

Stream realtime typesafe data from oRPC procedures with async generators, event validation, resume support, and cleanup on disconnect.

Sponsors

ScreenshotOne.comThe screenshot API for developersYuzuWe're hiring NYC based engineersMisskeyHQDecentralized microblogging SNS born on Earth

Overview

An AsyncIteratorObject is implemented as an asynchronous generator function (or a compatible implementation). In the example below, the handler emits a new event every second:

const example = os.handler(async function* ({ input, signal, lastEventId }) {
  while (true) {
    signal?.throwIfAborted()
    yield { message: 'Hello, world!' }
    await new Promise((resolve) => setTimeout(resolve, 1000))
  }
})

Learn how to consume an AsyncIteratorObject from the client in the client guide.

Validating Events

Use the built‑in asyncIteratorObject schema that works with any Standard Schema library to validate events.

import { asyncIteratorObject } from '@orpc/server'

const example = os
  .output(asyncIteratorObject(z.object({ message: z.string() })))
  .handler(async function* ({ input, signal, lastEventId }) {
    while (true) {
      signal?.throwIfAborted()
      yield { message: 'Hello, world!' }
      await new Promise((resolve) => setTimeout(resolve, 1000))
    }
  })

Last Event ID & Event Metadata

Using the withEventMeta helper, you can attach additional event metadata (such as an event ID or retry interval) to each event. When the client reconnects properly, the last received event ID is sent back to the server in lastEventId, allowing the stream to resume from where it left off.

When used with the Retry Plugin or EventSource, reconnection with the last event ID is handled automatically.

import { withEventMeta } from '@orpc/server'

const example = os.handler(async function* ({ input, signal, lastEventId }) {
  if (lastEventId) {
    // Resume streaming from lastEventId
  } else {
    while (true) {
      signal?.throwIfAborted()
      yield withEventMeta({ message: 'Hello, world!' }, { id: 'some-id', retry: 10_000 })
      await new Promise((resolve) => setTimeout(resolve, 1000))
    }
  }
})

Stop AsyncIteratorObject

To end the stream, use either a return or throw statement. oRPC marks the stream as completed when the handler returns.

This behavior is specific to oRPC. Standard SSE clients, such as EventSource, do not recognize this completion signal and will automatically attempt to reconnect. For details, see the Standard Server documentation.

const example = os.handler(async function* ({ input, signal, lastEventId }) {
  while (true) {
    signal?.throwIfAborted()

    if (done) {
      return
    }

    yield { message: 'Hello, world!' }
    await new Promise((resolve) => setTimeout(resolve, 1000))
  }
})

Signal and Side-Effects

When the client closes the connection or an unexpected error occurs, oRPC aborts the provided signal. Use it to exit loops and avoid resource leaks. Put cleanup logic in a finally block so it runs whether the stream ends normally, errors, or is cancelled.

const example = os.handler(async function* ({ input, signal, lastEventId }) {
  try {
    while (true) {
      signal?.throwIfAborted()
      yield { message: 'Hello, world!' }
      await new Promise((resolve) => setTimeout(resolve, 1000))
    }
  } finally {
    console.log('Cleanup logic here')
  }
})

In-process fetch calls and some runtimes behind the Fetch adapter only cancel the response body without aborting the signal. Use the Signal Sync Plugin to abort it.

Sponsors

ScreenshotOne.comThe screenshot API for developersYuzuWe're hiring NYC based engineers

Publisher Helper

You can combine the AsyncIteratorObject with the Publisher Helper to build real-time features like chat, notifications, or live updates with resume support.

const publisher = new MemoryPublisher<{
  'something-updated': {
    id: string
  }
}>()

const live = os.handler(async function* ({ input, signal, lastEventId }) {
  const iterator = publisher.subscribe('something-updated', { signal, lastEventId })
  for await (const payload of iterator) {
    // Handle payload here or yield directly to client
    yield payload
  }
})

const publish = os.input(z.object({ id: z.string() })).handler(async ({ input }) => {
  await publisher.publish('something-updated', { id: input.id })
})

Last updated on November 9, 2026

Was this page helpful?

Thanks for your feedback!

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.