Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

Streaming responses

Use managed typed iterators or native SSE, text, and byte streams.

Return a sequence of values when a response should arrive incrementally instead of as one completed JSON object.

Choose managed streaming

Declare output: streamOf(itemSchema) and return an async generator. The iterator is lazy, single-consumer, schema-validates each item, and retains invocation scope until completion or cancellation. The queue is bounded to 32 items and 4 MiB; a fully encoded item over 1 MiB fails immediately.

/** Supplies the report route with ten validated stream items, stopping when its invocation signal aborts. */
import { defineFunction, streamOf } from "@relkit/app/functions";
import { z } from "@relkit/app/schema";

const streamOrderReport = defineFunction({
  input: z.object({ reportId: z.string().min(1) }),
  output: streamOf(z.object({ completed: z.number().int(), total: z.number().int() })),
  handler: async function* (_input, context) {
    for (let completed = 1; completed <= 10; completed += 1) {
      // if (completed > 1) {
      //   await Bun.sleep(1_000);
      // }
      if (context.signal.aborted) return;
      yield { completed, total: completed };
    }
  },
});

export default streamOrderReport;

Use from the frontend

Expose that function through a route before pulling its contract:

/** Maps a report path identifier to the order stream and exposes its validated items as server-sent events. */
import { defineRoute, http } from "@relkit/app/routes";
import orders from "../../../services/orders/service.js";

export const GET = defineRoute({
  target: orders.streamOrderReport,
  request: http.input({ reportId: http.path("reportId") }),
  client: { operation: "query" },
  stream: { format: "sse" },
});

After frontend setup and pulling the commerce contract, render this component under the provider. The GET /reports/:reportId route exposes the streaming function:

"use client";

/** Consumes the commerce report route as a cancellable typed stream inside the client provider. */
import { useRelkitClient, useStream } from "@relkit/client/react";

/** Starts one report explicitly and displays its validated progress items.
 * @returns Start/cancel controls and visible progress or stream failure. */
export function Report() {
  const { status } = useRelkitClient();
  const report = useStream("GET /reports/:reportId");

  return (
    <section>
      <button
        disabled={status !== "ready"}
        onClick={() => void report.start({ reportId: "daily" })}
      >
        Run report
      </button>
      <button onClick={() => report.cancel()}>Cancel report</button>
      <p role="status">Completed: {report.items.map((item) => item.completed).join(", ")}</p>
      {report.error && <p role="alert">{String(report.error)}</p>}
    </section>
  );
}

streamOf supplies the item type, so report.items exposes each item’s completed field. start({ reportId }) begins the request; cancel() releases it. Use the domain input even when REST places a field in the URL. Streaming operations are excluded from finite useRoute queries.

Reference: useStream. Continue with stream lifecycle and failures.

Choose a native format

Managed oRPC iterators preserve typed completion and failure. Native SSE emits relkit-item and an explicit relkit-complete or sanitized relkit-error. Native text and bytes abort on a post-header application failure; because EOF cannot always distinguish failure, use managed streaming or SSE when callers need an explicit terminal result. Existing raw Hono routes remain the escape hatch.

Reference and source